diff --git a/api/core/v1alpha1/device_types.go b/api/core/v1alpha1/device_types.go index 218a891fc..b9b362029 100644 --- a/api/core/v1alpha1/device_types.go +++ b/api/core/v1alpha1/device_types.go @@ -128,6 +128,10 @@ type DeviceStatus struct { // +required Phase DevicePhase `json:"phase,omitempty"` + // Hostname is the hostname of the Device. + // +optional + Hostname string `json:"hostname,omitempty"` + // Manufacturer is the manufacturer of the Device. // +optional Manufacturer string `json:"manufacturer,omitempty"` diff --git a/api/core/v1alpha1/groupversion_info.go b/api/core/v1alpha1/groupversion_info.go index b8a066d42..3fdaf4800 100644 --- a/api/core/v1alpha1/groupversion_info.go +++ b/api/core/v1alpha1/groupversion_info.go @@ -80,8 +80,9 @@ const PhysicalInterfaceNeighborLabel = "networking.metal.ironcore.dev/interface- // PhysicalInterfaceNeighborRawAnnotation stores raw neighbor identification for interfaces // connected to unmanaged devices (devices without an Interface resource). -// The value format is "chassisID::portID" where: -// - chassisID: The LLDP chassis identifier (MAC address or system name) +// The value format is "(chassisID|sysName)::portID" where: +// - chassisID: The LLDP Chassis ID (e.g. MAC address) +// - sysName: The LLDP System Name (optional TLV) // - portID: The LLDP port identifier (interface name, alias, or MAC address) // // Example: "00:1a:2b:3c:4d:5e::Ethernet1/1" or "spine-switch-01::Ethernet48" diff --git a/api/core/v1alpha1/interface_types.go b/api/core/v1alpha1/interface_types.go index 18ddcd4f6..410593ee2 100644 --- a/api/core/v1alpha1/interface_types.go +++ b/api/core/v1alpha1/interface_types.go @@ -381,8 +381,162 @@ type InterfaceStatus struct { // This field only applies to physical interfaces that are part of an aggregate interface. // +optional MemberOf *LocalObjectReference `json:"memberOf,omitempty"` + + // Neighbors contains a list of neighbor interfaces connected to this interface and discovered with LLDP. + // If a single interface has multiple neighbor adjacencies, we validate each adjacency against the same one label/annotation. + // +optional + Neighbors []Neighbor `json:"neighbors,omitempty"` } +// Neighbor represents an LLDP neighbor discovered on an interface. +// It includes the results of the LLDP adjacency validation against the expected neighbor information from the interface's labels or annotations. +type Neighbor struct { + // ChassisID contains an octet string indicating the specific chassis ID of the neighbor. + // Its semantics are defined by the ChassisIDType field. + // +required + ChassisID string `json:"chassisId"` + + // ChassisIDType represents the chassis ID subtype. + // Full list of types can be found in IEEE 802.1AB-2016 Table 8-2. + // +required + // +kubebuilder:validation:Enum=ChassisComponent;InterfaceAlias;PortComponent;MACAddress;NetworkAddress;InterfaceName;Local + ChassisIDType ChassisIDType `json:"chassisIdType"` + + // PortID contains an octet string indicating the specific port ID of the neighbor. + // Its semantics are defined by the PortIDType field. + // +required + PortID string `json:"portId"` + + // PortIDType represents the port ID subtype. + // Full list of types can be found in IEEE 802.1AB-2016 Table 8-3. + // +required + // +kubebuilder:validation:Enum=InterfaceAlias;PortComponent;MACAddress;NetworkAddress;InterfaceName;AgentCircuitID;Local + PortIDType PortIDType `json:"portIdType"` + + // SystemName is an alpha-numeric string that indicates the system’s administratively assigned name. + // +optional + SystemName string `json:"systemName,omitempty"` + + // SystemDescription is a textual description of the neighbor, should include hardware and software information + // If the device supports IETF RFC 3418, this is the `sysDescr` + // +optional + SystemDescription string `json:"systemDescription,omitempty"` + + // PortDescription contains the port description of the neighbor port. + // If the device supports IETF RFC 2863, this is the `ifDescr` + // +optional + PortDescription string `json:"portDescription,omitempty"` + + // ExpirationTime is the time when the LLDP neighbor information expires. + // It is calculated based on the TTL. + // +required + ExpirationTime metav1.Time `json:"expirationTime"` + + // Validation indicates whether the LLDP neighbor information matches the information in the label or annotations of the interface. + // Empty when no validation source (label or annotation) is configured on the interface. + // +optional + Validation NeighborValidation `json:"validation,omitempty"` +} + +// ChassisIDType represents the chassis ID subtype for LLDP neighbor information. +// See IEEE 802.1AB-2016 section 8.5.2.2 for details. +type ChassisIDType string + +const ( + // ChassisIDTypeChassisComponent is `EntPhysicalAlias` when entPhysClass has a value of ‘chassis(3)’ (IETF RFC 6933) + ChassisIDTypeChassisComponent ChassisIDType = "ChassisComponent" + // ChassisIDTypeInterfaceAlias is `ifAlias` (IETF RFC 2863) + ChassisIDTypeInterfaceAlias ChassisIDType = "InterfaceAlias" + // ChassisIDTypePortComponent is `entPhysicalAlias` when `entPhysicalClass` has a value ‘port(10)’ or ‘backplane(4)’ (IETF RFC 6933) + ChassisIDTypePortComponent ChassisIDType = "PortComponent" + // ChassisIDTypeMACAddress is the MAC address (IEEE Std 802) + ChassisIDTypeMACAddress ChassisIDType = "MACAddress" + // ChassisIDTypeNetworkAddress is an octet string representation of a particular network family and address. + ChassisIDTypeNetworkAddress ChassisIDType = "NetworkAddress" + // ChassisIDTypeInterfaceName is `ifName` (IETF RFC 2863) + ChassisIDTypeInterfaceName ChassisIDType = "InterfaceName" + // ChassisIDTypeLocal is an alphanumeric string that and is locally assigned + ChassisIDTypeLocal ChassisIDType = "Local" +) + +func ChassisIDTypeFromValue(value uint8) (ChassisIDType, bool) { + switch value { + case 1: + return ChassisIDTypeChassisComponent, true + case 2: + return ChassisIDTypeInterfaceAlias, true + case 3: + return ChassisIDTypePortComponent, true + case 4: + return ChassisIDTypeMACAddress, true + case 5: + return ChassisIDTypeNetworkAddress, true + case 6: + return ChassisIDTypeInterfaceName, true + case 7: + return ChassisIDTypeLocal, true + default: + return "", false + } +} + +// PortIDType represents the port ID subtype for LLDP neighbor information. +// See IEEE 802.1AB-2016 section 8.5.3.2 for details. +type PortIDType string + +const ( + // PortIDTypeInterfaceAlias is `ifAlias` (IETF RFC 2863) + PortIDTypeInterfaceAlias PortIDType = "InterfaceAlias" + // PortIDTypePortComponent is `entPhysicalAlias` when `entPhysicalClass` has a value ‘port(10)’ or ‘backplane(4)’ (IETF RFC 6933) + PortIDTypePortComponent PortIDType = "PortComponent" + // PortIDTypeMACAddress is the MAC address (IEEE Std 802) + PortIDTypeMACAddress PortIDType = "MACAddress" + // PortIDTypeNetworkAddress is an octet string representation of a particular network family and address. + PortIDTypeNetworkAddress PortIDType = "NetworkAddress" + // PortIDTypeInterfaceName is `ifName` (IETF RFC 2863) + PortIDTypeInterfaceName PortIDType = "InterfaceName" + // PortIDTypeAgentCircuitID is the agent circuit ID (IETF RFC 3046) + PortIDTypeAgentCircuitID PortIDType = "AgentCircuitID" + // PortIDTypeLocal is an alphanumeric string that and is locally assigned + PortIDTypeLocal PortIDType = "Local" +) + +func PortIDTypeFromValue(value uint8) (PortIDType, bool) { + switch value { + case 1: + return PortIDTypeInterfaceAlias, true + case 2: + return PortIDTypePortComponent, true + case 3: + return PortIDTypeMACAddress, true + case 4: + return PortIDTypeNetworkAddress, true + case 5: + return PortIDTypeInterfaceName, true + case 6: + return PortIDTypeAgentCircuitID, true + case 7: + return PortIDTypeLocal, true + default: + return "", false + } +} + +// NeighborValidation represents the result of the validation of the LLDP neighbor information against the expected values from the interface's labels or annotations. +// +kubebuilder:validation:Enum=NotFound;Verified;DeviceMismatch;PortMismatch +type NeighborValidation string + +const ( + // NeighborNotFound indicates that the resource referenced in the PhysicalInterfaceNeighborLabel label could not be found. + NeighborNotFound NeighborValidation = "NotFound" + // NeighborVerified indicates that the LLDP neighbor information has been verified and matches the expected values. + NeighborVerified NeighborValidation = "Verified" + // NeighborDeviceMismatch indicates that the LLDP neighbor information does not match the expected values, indicating a potential misconfiguration or unexpected neighbor. + NeighborDeviceMismatch NeighborValidation = "DeviceMismatch" + // NeighborPortMismatch indicates that the LLDP neighbor information does not match the expected port information, indicating a potential misconfiguration or unexpected neighbor. + NeighborPortMismatch NeighborValidation = "PortMismatch" +) + // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:path=interfaces diff --git a/api/core/v1alpha1/zz_generated.deepcopy.go b/api/core/v1alpha1/zz_generated.deepcopy.go index f3ffed091..3571a97c0 100644 --- a/api/core/v1alpha1/zz_generated.deepcopy.go +++ b/api/core/v1alpha1/zz_generated.deepcopy.go @@ -1903,6 +1903,13 @@ func (in *InterfaceStatus) DeepCopyInto(out *InterfaceStatus) { *out = new(LocalObjectReference) **out = **in } + if in.Neighbors != nil { + in, out := &in.Neighbors, &out.Neighbors + *out = make([]Neighbor, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InterfaceStatus. @@ -2377,6 +2384,22 @@ func (in *NameServer) DeepCopy() *NameServer { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Neighbor) DeepCopyInto(out *Neighbor) { + *out = *in + in.ExpirationTime.DeepCopyInto(&out.ExpirationTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Neighbor. +func (in *Neighbor) DeepCopy() *Neighbor { + if in == nil { + return nil + } + out := new(Neighbor) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NetworkVirtualizationEdge) DeepCopyInto(out *NetworkVirtualizationEdge) { *out = *in diff --git a/charts/network-operator/templates/crd/devices.networking.metal.ironcore.dev.yaml b/charts/network-operator/templates/crd/devices.networking.metal.ironcore.dev.yaml index 2ebac4ace..a623817eb 100644 --- a/charts/network-operator/templates/crd/devices.networking.metal.ironcore.dev.yaml +++ b/charts/network-operator/templates/crd/devices.networking.metal.ironcore.dev.yaml @@ -372,6 +372,9 @@ spec: description: FirmwareVersion is the firmware version running on the Device. type: string + hostname: + description: Hostname is the hostname of the Device. + type: string lastRebootTime: description: LastRebootTime is the timestamp of the last reboot of the Device, if known. diff --git a/charts/network-operator/templates/crd/interfaces.networking.metal.ironcore.dev.yaml b/charts/network-operator/templates/crd/interfaces.networking.metal.ironcore.dev.yaml index 918df2e8d..fca4f0650 100644 --- a/charts/network-operator/templates/crd/interfaces.networking.metal.ironcore.dev.yaml +++ b/charts/network-operator/templates/crd/interfaces.networking.metal.ironcore.dev.yaml @@ -625,6 +625,89 @@ spec: - name type: object x-kubernetes-map-type: atomic + neighbors: + description: |- + Neighbors contains a list of neighbor interfaces connected to this interface and discovered with LLDP. + If a single interface has multiple neighbor adjacencies, we validate each adjacency against the same one label/annotation. + items: + description: |- + Neighbor represents an LLDP neighbor discovered on an interface. + It includes the results of the LLDP adjacency validation against the expected neighbor information from the interface's labels or annotations. + properties: + chassisId: + description: |- + ChassisID contains an octet string indicating the specific chassis ID of the neighbor. + Its semantics are defined by the ChassisIDType field. + type: string + chassisIdType: + description: |- + ChassisIDType represents the chassis ID subtype. + Full list of types can be found in IEEE 802.1AB-2016 Table 8-2. + enum: + - ChassisComponent + - InterfaceAlias + - PortComponent + - MACAddress + - NetworkAddress + - InterfaceName + - Local + type: string + expirationTime: + description: |- + ExpirationTime is the time when the LLDP neighbor information expires. + It is calculated based on the TTL. + format: date-time + type: string + portDescription: + description: |- + PortDescription contains the port description of the neighbor port. + If the device supports IETF RFC 2863, this is the `ifDescr` + type: string + portId: + description: |- + PortID contains an octet string indicating the specific port ID of the neighbor. + Its semantics are defined by the PortIDType field. + type: string + portIdType: + description: |- + PortIDType represents the port ID subtype. + Full list of types can be found in IEEE 802.1AB-2016 Table 8-3. + enum: + - InterfaceAlias + - PortComponent + - MACAddress + - NetworkAddress + - InterfaceName + - AgentCircuitID + - Local + type: string + systemDescription: + description: |- + SystemDescription is a textual description of the neighbor, should include hardware and software information + If the device supports IETF RFC 3418, this is the `sysDescr` + type: string + systemName: + description: SystemName is an alpha-numeric string that indicates + the system’s administratively assigned name. + type: string + validation: + description: |- + Validation indicates whether the LLDP neighbor information matches the information in the label or annotations of the interface. + Empty when no validation source (label or annotation) is configured on the interface. + enum: + - NotFound + - Verified + - DeviceMismatch + - PortMismatch + type: string + required: + - chassisId + - chassisIdType + - expirationTime + - portId + - portIdType + type: object + type: array type: object required: - spec diff --git a/config/crd/bases/networking.metal.ironcore.dev_devices.yaml b/config/crd/bases/networking.metal.ironcore.dev_devices.yaml index 2e4ea5d0b..9d07a1234 100644 --- a/config/crd/bases/networking.metal.ironcore.dev_devices.yaml +++ b/config/crd/bases/networking.metal.ironcore.dev_devices.yaml @@ -369,6 +369,9 @@ spec: description: FirmwareVersion is the firmware version running on the Device. type: string + hostname: + description: Hostname is the hostname of the Device. + type: string lastRebootTime: description: LastRebootTime is the timestamp of the last reboot of the Device, if known. diff --git a/config/crd/bases/networking.metal.ironcore.dev_interfaces.yaml b/config/crd/bases/networking.metal.ironcore.dev_interfaces.yaml index ab77c10b5..4903efa77 100644 --- a/config/crd/bases/networking.metal.ironcore.dev_interfaces.yaml +++ b/config/crd/bases/networking.metal.ironcore.dev_interfaces.yaml @@ -622,6 +622,89 @@ spec: - name type: object x-kubernetes-map-type: atomic + neighbors: + description: |- + Neighbors contains a list of neighbor interfaces connected to this interface and discovered with LLDP. + If a single interface has multiple neighbor adjacencies, we validate each adjacency against the same one label/annotation. + items: + description: |- + Neighbor represents an LLDP neighbor discovered on an interface. + It includes the results of the LLDP adjacency validation against the expected neighbor information from the interface's labels or annotations. + properties: + chassisId: + description: |- + ChassisID contains an octet string indicating the specific chassis ID of the neighbor. + Its semantics are defined by the ChassisIDType field. + type: string + chassisIdType: + description: |- + ChassisIDType represents the chassis ID subtype. + Full list of types can be found in IEEE 802.1AB-2016 Table 8-2. + enum: + - ChassisComponent + - InterfaceAlias + - PortComponent + - MACAddress + - NetworkAddress + - InterfaceName + - Local + type: string + expirationTime: + description: |- + ExpirationTime is the time when the LLDP neighbor information expires. + It is calculated based on the TTL. + format: date-time + type: string + portDescription: + description: |- + PortDescription contains the port description of the neighbor port. + If the device supports IETF RFC 2863, this is the `ifDescr` + type: string + portId: + description: |- + PortID contains an octet string indicating the specific port ID of the neighbor. + Its semantics are defined by the PortIDType field. + type: string + portIdType: + description: |- + PortIDType represents the port ID subtype. + Full list of types can be found in IEEE 802.1AB-2016 Table 8-3. + enum: + - InterfaceAlias + - PortComponent + - MACAddress + - NetworkAddress + - InterfaceName + - AgentCircuitID + - Local + type: string + systemDescription: + description: |- + SystemDescription is a textual description of the neighbor, should include hardware and software information + If the device supports IETF RFC 3418, this is the `sysDescr` + type: string + systemName: + description: SystemName is an alpha-numeric string that indicates + the system’s administratively assigned name. + type: string + validation: + description: |- + Validation indicates whether the LLDP neighbor information matches the information in the label or annotations of the interface. + Empty when no validation source (label or annotation) is configured on the interface. + enum: + - NotFound + - Verified + - DeviceMismatch + - PortMismatch + type: string + required: + - chassisId + - chassisIdType + - expirationTime + - portId + - portIdType + type: object + type: array type: object required: - spec diff --git a/docs/api-reference/index.md b/docs/api-reference/index.md index bdd381f63..b333d37e9 100644 --- a/docs/api-reference/index.md +++ b/docs/api-reference/index.md @@ -763,6 +763,29 @@ _Appears in:_ | `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#condition-v1-meta) array_ | The conditions are a list of status objects that describe the state of the Certificate. | | Optional: \{\}
| +#### ChassisIDType + +_Underlying type:_ _string_ + +ChassisIDType represents the chassis ID subtype for LLDP neighbor information. +See IEEE 802.1AB-2016 section 8.5.2.2 for details. + + + +_Appears in:_ +- [Neighbor](#neighbor) + +| Field | Description | +| --- | --- | +| `ChassisComponent` | ChassisIDTypeChassisComponent is `EntPhysicalAlias` when entPhysClass has a value of ‘chassis(3)’ (IETF RFC 6933)
| +| `InterfaceAlias` | ChassisIDTypeInterfaceAlias is `ifAlias` (IETF RFC 2863)
| +| `PortComponent` | ChassisIDTypePortComponent is `entPhysicalAlias` when `entPhysicalClass` has a value ‘port(10)’ or ‘backplane(4)’ (IETF RFC 6933)
| +| `MACAddress` | ChassisIDTypeMACAddress is the MAC address (IEEE Std 802)
| +| `NetworkAddress` | ChassisIDTypeNetworkAddress is an octet string representation of a particular network family and address.
| +| `InterfaceName` | ChassisIDTypeInterfaceName is `ifName` (IETF RFC 2863)
| +| `Local` | ChassisIDTypeLocal is an alphanumeric string that and is locally assigned
| + + #### ChecksumType _Underlying type:_ _string_ @@ -1038,6 +1061,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | | `phase` _[DevicePhase](#devicephase)_ | Phase represents the current phase of the Device. | Pending | Enum: [Pending Provisioning Running Failed Provisioned]
Required: \{\}
| +| `hostname` _string_ | Hostname is the hostname of the Device. | | Optional: \{\}
| | `manufacturer` _string_ | Manufacturer is the manufacturer of the Device. | | Optional: \{\}
| | `model` _string_ | Model is the model identifier of the Device. | | Optional: \{\}
| | `serialNumber` _string_ | SerialNumber is the serial number of the Device. | | Optional: \{\}
| @@ -1507,6 +1531,7 @@ _Appears in:_ | --- | --- | --- | --- | | `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#condition-v1-meta) array_ | The conditions are a list of status objects that describe the state of the Interface. | | Optional: \{\}
| | `memberOf` _[LocalObjectReference](#localobjectreference)_ | MemberOf references the aggregate interface this interface is a member of, if any.
This field only applies to physical interfaces that are part of an aggregate interface. | | Optional: \{\}
| +| `neighbors` _[Neighbor](#neighbor) array_ | Neighbors contains a list of neighbor interfaces connected to this interface and discovered with LLDP.
If a single interface has multiple neighbor adjacencies, we validate each adjacency against the same one label/annotation. | | Optional: \{\}
| #### InterfaceType @@ -1910,6 +1935,51 @@ _Appears in:_ | `vrfName` _string_ | The name of the vrf used to communicate with the DNS server. | | MaxLength: 63
MinLength: 1
Optional: \{\}
| +#### Neighbor + + + +Neighbor represents an LLDP neighbor discovered on an interface. +It includes the results of the LLDP adjacency validation against the expected neighbor information from the interface's labels or annotations. + + + +_Appears in:_ +- [InterfaceStatus](#interfacestatus) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `chassisId` _string_ | ChassisID contains an octet string indicating the specific chassis ID of the neighbor.
Its semantics are defined by the ChassisIDType field. | | Required: \{\}
| +| `chassisIdType` _[ChassisIDType](#chassisidtype)_ | ChassisIDType represents the chassis ID subtype.
Full list of types can be found in IEEE 802.1AB-2016 Table 8-2. | | Enum: [ChassisComponent InterfaceAlias PortComponent MACAddress NetworkAddress InterfaceName Local]
Required: \{\}
| +| `portId` _string_ | PortID contains an octet string indicating the specific port ID of the neighbor.
Its semantics are defined by the PortIDType field. | | Required: \{\}
| +| `portIdType` _[PortIDType](#portidtype)_ | PortIDType represents the port ID subtype.
Full list of types can be found in IEEE 802.1AB-2016 Table 8-3. | | Enum: [InterfaceAlias PortComponent MACAddress NetworkAddress InterfaceName AgentCircuitID Local]
Required: \{\}
| +| `systemName` _string_ | SystemName is an alpha-numeric string that indicates the system’s administratively assigned name. | | Optional: \{\}
| +| `systemDescription` _string_ | SystemDescription is a textual description of the neighbor, should include hardware and software information
If the device supports IETF RFC 3418, this is the `sysDescr` | | Optional: \{\}
| +| `portDescription` _string_ | PortDescription contains the port description of the neighbor port.
If the device supports IETF RFC 2863, this is the `ifDescr` | | Optional: \{\}
| +| `expirationTime` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#time-v1-meta)_ | ExpirationTime is the time when the LLDP neighbor information expires.
It is calculated based on the TTL. | | Required: \{\}
| +| `validation` _[NeighborValidation](#neighborvalidation)_ | Validation indicates whether the LLDP neighbor information matches the information in the label or annotations of the interface.
Empty when no validation source (label or annotation) is configured on the interface. | | Enum: [NotFound Verified DeviceMismatch PortMismatch]
Optional: \{\}
| + + +#### NeighborValidation + +_Underlying type:_ _string_ + +NeighborValidation represents the result of the validation of the LLDP neighbor information against the expected values from the interface's labels or annotations. + +_Validation:_ +- Enum: [NotFound Verified DeviceMismatch PortMismatch] + +_Appears in:_ +- [Neighbor](#neighbor) + +| Field | Description | +| --- | --- | +| `NotFound` | NeighborNotFound indicates that the resource referenced in the PhysicalInterfaceNeighborLabel label could not be found.
| +| `Verified` | NeighborVerified indicates that the LLDP neighbor information has been verified and matches the expected values.
| +| `DeviceMismatch` | NeighborDeviceMismatch indicates that the LLDP neighbor information does not match the expected values, indicating a potential misconfiguration or unexpected neighbor.
| +| `PortMismatch` | NeighborPortMismatch indicates that the LLDP neighbor information does not match the expected port information, indicating a potential misconfiguration or unexpected neighbor.
| + + #### NetworkVirtualizationEdge @@ -2274,6 +2344,29 @@ _Appears in:_ | `actions` _[PolicyActions](#policyactions)_ | Actions define what to do when conditions match. | | Required: \{\}
| +#### PortIDType + +_Underlying type:_ _string_ + +PortIDType represents the port ID subtype for LLDP neighbor information. +See IEEE 802.1AB-2016 section 8.5.3.2 for details. + + + +_Appears in:_ +- [Neighbor](#neighbor) + +| Field | Description | +| --- | --- | +| `InterfaceAlias` | PortIDTypeInterfaceAlias is `ifAlias` (IETF RFC 2863)
| +| `PortComponent` | PortIDTypePortComponent is `entPhysicalAlias` when `entPhysicalClass` has a value ‘port(10)’ or ‘backplane(4)’ (IETF RFC 6933)
| +| `MACAddress` | PortIDTypeMACAddress is the MAC address (IEEE Std 802)
| +| `NetworkAddress` | PortIDTypeNetworkAddress is an octet string representation of a particular network family and address.
| +| `InterfaceName` | PortIDTypeInterfaceName is `ifName` (IETF RFC 2863)
| +| `AgentCircuitID` | PortIDTypeAgentCircuitID is the agent circuit ID (IETF RFC 3046)
| +| `Local` | PortIDTypeLocal is an alphanumeric string that and is locally assigned
| + + #### PrefixEntry diff --git a/internal/controller/cisco/nx/bordergateway_controller.go b/internal/controller/cisco/nx/bordergateway_controller.go index 6a7f9a380..3470a1a61 100644 --- a/internal/controller/cisco/nx/bordergateway_controller.go +++ b/internal/controller/cisco/nx/bordergateway_controller.go @@ -626,7 +626,8 @@ func (r *BorderGatewayReconciler) deviceToBorderGateways(ctx context.Context, ob log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(nxv1alpha1.BorderGatewayList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { diff --git a/internal/controller/cisco/nx/system_controller.go b/internal/controller/cisco/nx/system_controller.go index ca42c1fd8..71831b30d 100644 --- a/internal/controller/cisco/nx/system_controller.go +++ b/internal/controller/cisco/nx/system_controller.go @@ -297,7 +297,8 @@ func (r *SystemReconciler) deviceToSystems(ctx context.Context, obj client.Objec log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(nxv1alpha1.SystemList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { diff --git a/internal/controller/cisco/nx/vpcdomain_controller.go b/internal/controller/cisco/nx/vpcdomain_controller.go index 6eb7b85e1..8f888da1c 100644 --- a/internal/controller/cisco/nx/vpcdomain_controller.go +++ b/internal/controller/cisco/nx/vpcdomain_controller.go @@ -463,7 +463,8 @@ func (r *VPCDomainReconciler) mapInterfaceToVPCDomain(ctx context.Context, obj c log := ctrl.LoggerFrom(ctx, "Interface", klog.KObj(iface)) list := new(nxv1alpha1.VPCDomainList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(iface.Namespace), client.MatchingFields{vpcDomainPeerLinkRefKey: iface.Name}, ); err != nil { @@ -507,7 +508,8 @@ func (r *VPCDomainReconciler) deviceToVPCDomains(ctx context.Context, obj client log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(nxv1alpha1.VPCDomainList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { diff --git a/internal/controller/core/acl_controller.go b/internal/controller/core/acl_controller.go index 97d8eb0d1..20c5de8a5 100644 --- a/internal/controller/core/acl_controller.go +++ b/internal/controller/core/acl_controller.go @@ -333,7 +333,8 @@ func (r *AccessControlListReconciler) deviceToAccessControlLists(ctx context.Con log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(v1alpha1.AccessControlListList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { diff --git a/internal/controller/core/banner_controller.go b/internal/controller/core/banner_controller.go index 83db2f8c3..6641e4131 100644 --- a/internal/controller/core/banner_controller.go +++ b/internal/controller/core/banner_controller.go @@ -348,7 +348,8 @@ func (r *BannerReconciler) deviceToBanners(ctx context.Context, obj client.Objec log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(v1alpha1.BannerList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { diff --git a/internal/controller/core/bgp_controller.go b/internal/controller/core/bgp_controller.go index 1ee25669d..a273b252c 100644 --- a/internal/controller/core/bgp_controller.go +++ b/internal/controller/core/bgp_controller.go @@ -518,7 +518,8 @@ func (r *BGPReconciler) deviceToBGPs(ctx context.Context, obj client.Object) []c log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(v1alpha1.BGPList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { @@ -583,7 +584,8 @@ func (r *BGPReconciler) vrfToBGPs(ctx context.Context, obj client.Object) []ctrl log := ctrl.LoggerFrom(ctx, "VRF", klog.KObj(vrf)) list := new(v1alpha1.BGPList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(vrf.Namespace), client.MatchingFields{bgpVrfRefIndexKey: vrf.Name}, ); err != nil { @@ -615,7 +617,8 @@ func (r *BGPReconciler) routingPolicyToBGPs(ctx context.Context, obj client.Obje log := ctrl.LoggerFrom(ctx, "RoutingPolicy", klog.KObj(rp)) list := new(v1alpha1.BGPList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(rp.Namespace), client.MatchingFields{bgpRedistributeDirectRoutePolicyIndexKey: rp.Name}, ); err != nil { diff --git a/internal/controller/core/bgp_peer_controller.go b/internal/controller/core/bgp_peer_controller.go index 34e1f423a..71de26336 100644 --- a/internal/controller/core/bgp_peer_controller.go +++ b/internal/controller/core/bgp_peer_controller.go @@ -693,7 +693,8 @@ func (r *BGPPeerReconciler) deviceToBGPPeers(ctx context.Context, obj client.Obj log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(v1alpha1.BGPPeerList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { @@ -758,7 +759,8 @@ func (r *BGPPeerReconciler) bgpToBGPPeers(ctx context.Context, obj client.Object log := ctrl.LoggerFrom(ctx, "BGP", klog.KObj(bgp)) list := new(v1alpha1.BGPPeerList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(bgp.Namespace), client.MatchingFields{bgpPeerBGPRefIndexKey: bgp.Name}, ); err != nil { @@ -791,7 +793,8 @@ func (r *BGPPeerReconciler) vrfToBGPPeers(ctx context.Context, obj client.Object log := ctrl.LoggerFrom(ctx, "VRF", klog.KObj(vrf)) bgpList := new(v1alpha1.BGPList) - if err := r.List(ctx, bgpList, + if err := r.List( + ctx, bgpList, client.InNamespace(vrf.Namespace), client.MatchingFields{bgpVrfRefIndexKey: vrf.Name}, ); err != nil { @@ -802,7 +805,8 @@ func (r *BGPPeerReconciler) vrfToBGPPeers(ctx context.Context, obj client.Object var requests []ctrl.Request for _, bgp := range bgpList.Items { peerList := new(v1alpha1.BGPPeerList) - if err := r.List(ctx, peerList, + if err := r.List( + ctx, peerList, client.InNamespace(vrf.Namespace), client.MatchingFields{bgpPeerBGPRefIndexKey: bgp.Name}, ); err != nil { @@ -835,7 +839,8 @@ func (r *BGPPeerReconciler) routingPolicyToBGPPeers(ctx context.Context, obj cli log := ctrl.LoggerFrom(ctx, "RoutingPolicy", klog.KObj(rp)) list := new(v1alpha1.BGPPeerList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(rp.Namespace), client.MatchingFields{bgpPeerRoutingPolicyRefIndexKey: rp.Name}, ); err != nil { diff --git a/internal/controller/core/certificate_controller.go b/internal/controller/core/certificate_controller.go index 4c1a000c6..8aede6f75 100644 --- a/internal/controller/core/certificate_controller.go +++ b/internal/controller/core/certificate_controller.go @@ -342,7 +342,8 @@ func (r *CertificateReconciler) deviceToCertificates(ctx context.Context, obj cl log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(v1alpha1.CertificateList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { diff --git a/internal/controller/core/device_controller.go b/internal/controller/core/device_controller.go index 111e98e5b..c05ef0e96 100644 --- a/internal/controller/core/device_controller.go +++ b/internal/controller/core/device_controller.go @@ -297,6 +297,7 @@ func (r *DeviceReconciler) reconcile(ctx context.Context, device *v1alpha1.Devic if err != nil { return fmt.Errorf("failed to get device info: %w", err) } + device.Status.Hostname = info.Hostname device.Status.Manufacturer = info.Manufacturer device.Status.Model = info.Model device.Status.SerialNumber = info.SerialNumber diff --git a/internal/controller/core/dhcprelay_controller.go b/internal/controller/core/dhcprelay_controller.go index 18128c201..28d787911 100644 --- a/internal/controller/core/dhcprelay_controller.go +++ b/internal/controller/core/dhcprelay_controller.go @@ -116,7 +116,7 @@ func (r *DHCPRelayReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( if err := r.Locker.AcquireLock(ctx, device.Name, "dhcprelay-controller"); err != nil { if errors.Is(err, resourcelock.ErrLockAlreadyHeld) { log.V(3).Info("Device is already locked, requeuing reconciliation") - return ctrl.Result{RequeueAfter: time.Second}, nil + return ctrl.Result{RequeueAfter: Jitter(time.Second), Priority: new(LockWaitPriorityDefault)}, nil } log.Error(err, "Failed to acquire device lock") return ctrl.Result{}, err @@ -550,7 +550,8 @@ func (r *DHCPRelayReconciler) reconcileVRFRef(ctx context.Context, s *dhcprelayS func (r *DHCPRelayReconciler) validateUniqueResourcePerDevice(ctx context.Context, s *dhcprelayScope) error { var list v1alpha1.DHCPRelayList - if err := r.List(ctx, &list, + if err := r.List( + ctx, &list, client.InNamespace(s.DHCPRelay.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: s.Device.Name}, ); err != nil { @@ -625,7 +626,8 @@ func (r *DHCPRelayReconciler) deviceToDHCPRelays(ctx context.Context, obj client log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(v1alpha1.DHCPRelayList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { @@ -657,7 +659,8 @@ func (r *DHCPRelayReconciler) interfaceToDHCPRelays(ctx context.Context, obj cli log := ctrl.LoggerFrom(ctx, "Interface", klog.KObj(intf)) list := new(v1alpha1.DHCPRelayList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(intf.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: intf.Spec.DeviceRef.Name}, ); err != nil { @@ -694,7 +697,8 @@ func (r *DHCPRelayReconciler) vrfToDHCPRelays(ctx context.Context, obj client.Ob log := ctrl.LoggerFrom(ctx, "VRF", klog.KObj(vrf)) list := new(v1alpha1.DHCPRelayList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(vrf.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: vrf.Spec.DeviceRef.Name}, ); err != nil { diff --git a/internal/controller/core/dhcprelay_controller_test.go b/internal/controller/core/dhcprelay_controller_test.go index 6554eb8b1..e440f1b03 100644 --- a/internal/controller/core/dhcprelay_controller_test.go +++ b/internal/controller/core/dhcprelay_controller_test.go @@ -1122,5 +1122,71 @@ var _ = Describe("DHCPRelay Controller", func() { g.Expect(cond.Message).To(ContainSubstring("not configured")) }).Should(Succeed()) }) + + It("Should re-reconcile DHCPRelay when Interface becomes configured (watch trigger)", func() { + By("Creating DHCPRelay referencing a non-configured Interface") + dhcprelay := &v1alpha1.DHCPRelay{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-dhcprelay-intfnr-watch-", + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.DHCPRelaySpec{ + DeviceRef: v1alpha1.LocalObjectReference{Name: deviceName}, + Servers: []string{"192.168.1.1"}, + InterfaceRefs: []v1alpha1.LocalObjectReference{ + {Name: interfaceName}, + }, + }, + } + Expect(k8sClient.Create(ctx, dhcprelay)).To(Succeed()) + resourceName = dhcprelay.Name + resourceKey = client.ObjectKey{Name: resourceName, Namespace: metav1.NamespaceDefault} + + By("Verifying DHCPRelay is not ready due to non-configured Interface") + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, resourceKey, dhcprelay) + g.Expect(err).NotTo(HaveOccurred()) + + cond := meta.FindStatusCondition(dhcprelay.Status.Conditions, v1alpha1.ConfiguredCondition) + g.Expect(cond).ToNot(BeNil()) + g.Expect(cond.Status).To(Equal(metav1.ConditionFalse)) + g.Expect(cond.Reason).To(Equal(v1alpha1.WaitingForDependenciesReason)) + }).Should(Succeed()) + + By("Creating the VRF to make the Interface configured") + vrf := &v1alpha1.VRF{ + ObjectMeta: metav1.ObjectMeta{ + Name: nonExistentVrfName, + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.VRFSpec{ + DeviceRef: v1alpha1.LocalObjectReference{Name: deviceName}, + Name: "VRF-TEST", + }, + } + Expect(k8sClient.Create(ctx, vrf)).To(Succeed()) + + By("Waiting for Interface to become configured") + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, interfaceKey, intf) + g.Expect(err).NotTo(HaveOccurred()) + cond := meta.FindStatusCondition(intf.Status.Conditions, v1alpha1.ConfiguredCondition) + g.Expect(cond).ToNot(BeNil()) + g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + }).Should(Succeed()) + + By("Verifying DHCPRelay becomes ready after Interface is configured (watch triggered re-reconciliation)") + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, resourceKey, dhcprelay) + g.Expect(err).NotTo(HaveOccurred()) + + cond := meta.FindStatusCondition(dhcprelay.Status.Conditions, v1alpha1.ReadyCondition) + g.Expect(cond).ToNot(BeNil()) + g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + }).Should(Succeed()) + + By("Cleaning up the VRF resource") + Expect(k8sClient.Delete(ctx, vrf)).To(Succeed()) + }) }) }) diff --git a/internal/controller/core/dns_controller.go b/internal/controller/core/dns_controller.go index 7e9d15850..36c9e1758 100644 --- a/internal/controller/core/dns_controller.go +++ b/internal/controller/core/dns_controller.go @@ -324,7 +324,8 @@ func (r *DNSReconciler) deviceToDNSs(ctx context.Context, obj client.Object) []c log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(v1alpha1.DNSList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { diff --git a/internal/controller/core/evpninstance_controller.go b/internal/controller/core/evpninstance_controller.go index 98d73dc49..48bf64195 100644 --- a/internal/controller/core/evpninstance_controller.go +++ b/internal/controller/core/evpninstance_controller.go @@ -463,7 +463,8 @@ func (r *EVPNInstanceReconciler) deviceToEVPNInstances(ctx context.Context, obj log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(v1alpha1.EVPNInstanceList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { diff --git a/internal/controller/core/interface_controller.go b/internal/controller/core/interface_controller.go index 9cda93ac8..be04340be 100644 --- a/internal/controller/core/interface_controller.go +++ b/internal/controller/core/interface_controller.go @@ -9,6 +9,7 @@ import ( "fmt" "net/netip" "slices" + "strings" "time" "k8s.io/apimachinery/pkg/api/equality" @@ -289,7 +290,12 @@ func (r *InterfaceReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Man } bldr := ctrl.NewControllerManagedBy(mgr). - For(&v1alpha1.Interface{}). + For(&v1alpha1.Interface{}, builder.WithPredicates(predicate.Or( + predicate.GenerationChangedPredicate{}, + predicate.LabelChangedPredicate{}, + predicate.AnnotationChangedPredicate{}, + interfaceUpdatePredicate{}, + ))). Named("interface"). WithEventFilter(filter) @@ -417,6 +423,43 @@ func (r *InterfaceReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Man Complete(r) } +// interfaceUpdatePredicate passes status-only updates through unless the +// neighbor ExpirationTime is the only change. Without this filter the status +// patch would immediately trigger a redundant second reconcile because the +// controller updates the ExpirationTime in the status during every reconcile loop. +// This would cause repeated reconciles every time the controller tries to update +// the status, even if there are no changes to the spec. +type interfaceUpdatePredicate struct { + predicate.Funcs +} + +// Update implements predicate.Predicate. +func (interfaceUpdatePredicate) Update(e event.UpdateEvent) bool { + oldIntf, ok := e.ObjectOld.(*v1alpha1.Interface) + if !ok { + return true + } + newIntf, ok := e.ObjectNew.(*v1alpha1.Interface) + if !ok { + return true + } + // Always reconcile if conditions haven't been fully initialized. + // InitializeConditions adds Ready/Configured/Operational, and paused.EnsureCondition + // adds Paused. Until all 4 are present, we must allow reconciles to complete setup. + if len(newIntf.Status.Conditions) < 4 { + return true + } + oldStatus := oldIntf.Status.DeepCopy() + newStatus := newIntf.Status.DeepCopy() + for i := range oldStatus.Neighbors { + oldStatus.Neighbors[i].ExpirationTime = metav1.Time{} + } + for i := range newStatus.Neighbors { + newStatus.Neighbors[i].ExpirationTime = metav1.Time{} + } + return !equality.Semantic.DeepEqual(oldStatus, newStatus) +} + // scope holds the different objects that are read and used during the reconcile. type scope struct { Device *v1alpha1.Device @@ -541,7 +584,22 @@ func (r *InterfaceReconciler) reconcile(ctx context.Context, s *scope) (reterr e return fmt.Errorf("failed to get interface status: %w", err) } - cond = metav1.Condition{ + r.reconcileInterfaceStatus(ctx, s, &status) + + return nil +} + +func (r *InterfaceReconciler) reconcileInterfaceStatus(ctx context.Context, s *scope, status *provider.InterfaceStatus) { + // Neighbor adjacencies is only metadata and should not prevent reconciliation + if s.Interface.Spec.Type == v1alpha1.InterfaceTypePhysical && len(status.LLDPAdjacencies) > 0 { + if err := r.updateNeighborAdjacenciesStatus(ctx, s, status); err != nil { + ctrl.LoggerFrom(ctx).Error(err, "failed to update neighbor adjacency status", "interface", klog.KObj(s.Interface)) + } + } else { + s.Interface.Status.Neighbors = nil + } + + cond := metav1.Condition{ Type: v1alpha1.OperationalCondition, Status: metav1.ConditionTrue, Reason: v1alpha1.OperationalReason, @@ -556,8 +614,135 @@ func (r *InterfaceReconciler) reconcile(ctx context.Context, s *scope) (reterr e cond.Message = fmt.Sprintf("Device returned %q", status.OperMessage) } conditions.Set(s.Interface, cond) +} - return nil +// updateNeighborAdjacenciesStatus updates the Interface status with the LLDP neighbor adjacencies returned by the provider. +// It validates the adjacencies by looking at the corresponding label/annotation. +// It first attempts to validate through label (neighbor is managed by the operator and exists as a kubernetes resource). +// If that fails, it attempts to validate through annotation (neighbor is not managed by the operator). +// Only returns an error if there is an issue during the validation process, but does not return an error if the validation fails (i.e. the adjacency is marked as invalid). +func (r *InterfaceReconciler) updateNeighborAdjacenciesStatus(ctx context.Context, s *scope, status *provider.InterfaceStatus) error { + type neighborKey struct{ ChassisID, PortID string } + + existingNeighbors := make(map[neighborKey]v1alpha1.Neighbor) + for _, n := range s.Interface.Status.Neighbors { + existingNeighbors[neighborKey{n.ChassisID, n.PortID}] = n + } + + log := ctrl.LoggerFrom(ctx) + if len(status.LLDPAdjacencies) > 1 { + log.V(1).Info("Multiple LLDP adjacencies found for a single interface, will validate each adjacency against one single label/annotation", "interface", klog.KObj(s.Interface), "adjacencyCount", len(status.LLDPAdjacencies)) + } + + var errs []error + neighbors := make([]v1alpha1.Neighbor, 0, len(status.LLDPAdjacencies)) + for _, adj := range status.LLDPAdjacencies { + chassisIDType, ok := v1alpha1.ChassisIDTypeFromValue(adj.ChassisIDType) + if !ok { + log.V(1).Info("Skipping LLDP adjacency with unknown chassis ID type", "chassisID", adj.ChassisID, "chassisIDType", adj.ChassisIDType) + continue + } + portIDType, ok := v1alpha1.PortIDTypeFromValue(adj.PortIDType) + if !ok { + log.V(1).Info("Skipping LLDP adjacency with unknown port ID type", "chassisID", adj.ChassisID, "portIDType", adj.PortIDType) + continue + } + + adjacency := v1alpha1.Neighbor{ + SystemName: adj.SysName, + SystemDescription: adj.SysDescription, + ChassisID: adj.ChassisID, + ChassisIDType: chassisIDType, + PortID: adj.PortID, + PortIDType: portIDType, + PortDescription: adj.PortDescription, + ExpirationTime: metav1.NewTime(time.Now().Add(adj.TTL).Truncate(time.Second)), + } + + // NOTE: the operator runs a single provider currently, so s.Provider is used for the + // remote device as well. If multi-provider support is added, the remote device's + // provider must be resolved here instead. + if neighborLabelValue, ok := s.Interface.Labels[v1alpha1.PhysicalInterfaceNeighborLabel]; ok { + var err error + if adjacency.Validation, err = r.validateLLDPAdjacencyThroughLabel(ctx, s.Provider, s.Interface, &adjacency, neighborLabelValue); err != nil { + errs = append(errs, fmt.Errorf("failed to validate LLDP adjacency %q/%q through label %q: %w", adj.ChassisID, adj.PortID, neighborLabelValue, err)) + } + } + + if neighborAnnotationValue, ok := s.Interface.Annotations[v1alpha1.PhysicalInterfaceNeighborRawAnnotation]; ok && adjacency.Validation == "" { + var err error + if adjacency.Validation, err = r.validateLLDPAdjacencyThroughAnnotation(ctx, s.Interface, &adjacency, neighborAnnotationValue); err != nil { + errs = append(errs, fmt.Errorf("failed to validate LLDP adjacency %q/%q through annotation %q: %w", adj.ChassisID, adj.PortID, neighborAnnotationValue, err)) + } + } + neighbors = append(neighbors, adjacency) + } + + s.Interface.Status.Neighbors = neighbors + + return kerrors.NewAggregate(errs) +} + +func (r *InterfaceReconciler) validateLLDPAdjacencyThroughLabel(ctx context.Context, remoteProvider provider.InterfaceProvider, intf *v1alpha1.Interface, n *v1alpha1.Neighbor, label string) (v1alpha1.NeighborValidation, error) { + key := client.ObjectKey{ + Name: label, + Namespace: intf.Namespace, + } + + remoteIntf := new(v1alpha1.Interface) + if err := r.Get(ctx, key, remoteIntf); err != nil { + if !apierrors.IsNotFound(err) { + return "", fmt.Errorf("failed to get neighbor interface %w", err) + } + return v1alpha1.NeighborNotFound, nil + } + + log := ctrl.LoggerFrom(ctx, "LLDP validation", klog.KObj(intf)) + + remoteDevice, err := deviceutil.GetOwnerDevice(ctx, r, remoteIntf) + if err != nil { + return "", fmt.Errorf("could not find the device owning interface %q: %w", remoteIntf.Name, err) + } + + if remoteDevice.Status.Hostname == "" { + return "", fmt.Errorf("the neighbor device does not have a hostname yet, cannot validate adjacency: neighborInterface=%q", remoteIntf.Name) + } + + if remoteDevice.Status.Hostname != n.SystemName { + log.V(1).Info("the neighbor device hostname does not match", "expected", n.SystemName, "actual", remoteDevice.Status.Hostname) + return v1alpha1.NeighborDeviceMismatch, nil + } + + equal, err := remoteProvider.InterfaceNameEqual(ctx, remoteIntf.Spec.Name, n.PortID) + if err != nil { + return "", fmt.Errorf("failed to compare interface names %q and %q: %w", remoteIntf.Spec.Name, n.PortID, err) + } + if !equal { + log.V(1).Info("the neighbor interface name does not match", "expected", n.PortID, "actual", remoteIntf.Spec.Name) + return v1alpha1.NeighborPortMismatch, nil + } + + return v1alpha1.NeighborVerified, nil +} + +func (r *InterfaceReconciler) validateLLDPAdjacencyThroughAnnotation(ctx context.Context, intf *v1alpha1.Interface, n *v1alpha1.Neighbor, annotation string) (v1alpha1.NeighborValidation, error) { + remoteDeviceID, remotePortID, ok := strings.Cut(annotation, "::") + if !ok || remoteDeviceID == "" || remotePortID == "" { + return "", errors.New("invalid neighbor annotation value, expected format is ::") + } + + log := ctrl.LoggerFrom(ctx, "LLDP validation", klog.KObj(intf)) + if remoteDeviceID != n.ChassisID && remoteDeviceID != n.SystemName { + log.V(1).Info("the neighbor device identifier does not match", "annotationValue", remoteDeviceID, "chassisID", n.ChassisID, "systemName", n.SystemName) + return v1alpha1.NeighborDeviceMismatch, nil + } + + if remotePortID != n.PortID { + log.V(1).Info("the neighbor port identifier does not match", "annotationValue", remotePortID, "portID", n.PortID) + return v1alpha1.NeighborPortMismatch, nil + } + + return v1alpha1.NeighborVerified, nil } func (r *InterfaceReconciler) reconcileIPv4(ctx context.Context, s *scope) (provider.IPv4, error) { @@ -1194,7 +1379,8 @@ func (r *InterfaceReconciler) deviceToInterfaces(ctx context.Context, obj client log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) interfaces := new(v1alpha1.InterfaceList) - if err := r.List(ctx, interfaces, + if err := r.List( + ctx, interfaces, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { diff --git a/internal/controller/core/interface_controller_test.go b/internal/controller/core/interface_controller_test.go index 9b9c3af26..0107f8690 100644 --- a/internal/controller/core/interface_controller_test.go +++ b/internal/controller/core/interface_controller_test.go @@ -5,12 +5,14 @@ package core import ( "net/netip" + "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" "github.com/ironcore-dev/network-operator/api/core/v1alpha1" ) @@ -1253,4 +1255,122 @@ var _ = Describe("Interface Controller", func() { }).Should(Succeed()) }) }) + + Context("Interface Update Predicate", func() { + var p interfaceUpdatePredicate + + BeforeEach(func() { + p = interfaceUpdatePredicate{} + }) + + It("Should allow update when Neighbors field is added", func() { + oldIntf := &v1alpha1.Interface{ + Status: v1alpha1.InterfaceStatus{ + Neighbors: nil, + }, + } + newIntf := &v1alpha1.Interface{ + Status: v1alpha1.InterfaceStatus{ + Neighbors: []v1alpha1.Neighbor{{ + SystemName: "switch-1", + ChassisID: "00:11:22:33:44:55", + PortID: "Eth1/1", + }}, + }, + } + e := event.UpdateEvent{ObjectOld: oldIntf, ObjectNew: newIntf} + Expect(p.Update(e)).To(BeTrue()) + }) + + It("Should allow update when neighbor SystemName changes", func() { + oldIntf := &v1alpha1.Interface{ + Status: v1alpha1.InterfaceStatus{ + Neighbors: []v1alpha1.Neighbor{{ + SystemName: "switch-1", + ChassisID: "00:11:22:33:44:55", + PortID: "Eth1/1", + ExpirationTime: metav1.NewTime(time.Now()), + }}, + }, + } + newIntf := oldIntf.DeepCopy() + newIntf.Status.Neighbors[0].SystemName = "switch-2" + + e := event.UpdateEvent{ObjectOld: oldIntf, ObjectNew: newIntf} + Expect(p.Update(e)).To(BeTrue()) + }) + + It("Should block update when only ExpirationTime changes", func() { + now := time.Now() + conditions := []metav1.Condition{ + {Type: v1alpha1.ReadyCondition, Status: metav1.ConditionTrue}, + {Type: v1alpha1.ConfiguredCondition, Status: metav1.ConditionTrue}, + {Type: v1alpha1.OperationalCondition, Status: metav1.ConditionTrue}, + {Type: v1alpha1.PausedCondition, Status: metav1.ConditionFalse}, + } + oldIntf := &v1alpha1.Interface{ + Status: v1alpha1.InterfaceStatus{ + Conditions: conditions, + Neighbors: []v1alpha1.Neighbor{{ + SystemName: "switch-1", + ChassisID: "00:11:22:33:44:55", + PortID: "Eth1/1", + ExpirationTime: metav1.NewTime(now), + }}, + }, + } + newIntf := oldIntf.DeepCopy() + newIntf.Status.Neighbors[0].ExpirationTime = metav1.NewTime(now.Add(120 * time.Second)) + + e := event.UpdateEvent{ObjectOld: oldIntf, ObjectNew: newIntf} + Expect(p.Update(e)).To(BeFalse()) + }) + + It("Should allow update when neighbor is removed", func() { + oldIntf := &v1alpha1.Interface{ + Status: v1alpha1.InterfaceStatus{ + Neighbors: []v1alpha1.Neighbor{{SystemName: "switch-1"}}, + }, + } + newIntf := &v1alpha1.Interface{ + Status: v1alpha1.InterfaceStatus{ + Neighbors: nil, + }, + } + e := event.UpdateEvent{ObjectOld: oldIntf, ObjectNew: newIntf} + Expect(p.Update(e)).To(BeTrue()) + }) + + It("Should allow update when generation changes", func() { + oldIntf := &v1alpha1.Interface{ + ObjectMeta: metav1.ObjectMeta{Generation: 1}, + Status: v1alpha1.InterfaceStatus{ + Neighbors: []v1alpha1.Neighbor{{ + ExpirationTime: metav1.NewTime(time.Now()), + }}, + }, + } + newIntf := oldIntf.DeepCopy() + newIntf.Generation = 2 + newIntf.Status.Neighbors[0].ExpirationTime = metav1.NewTime(time.Now().Add(120 * time.Second)) + + e := event.UpdateEvent{ObjectOld: oldIntf, ObjectNew: newIntf} + Expect(p.Update(e)).To(BeTrue()) + }) + + It("Should allow update when conditions are not fully initialized", func() { + oldIntf := &v1alpha1.Interface{ + Status: v1alpha1.InterfaceStatus{ + Conditions: []metav1.Condition{ + {Type: v1alpha1.ReadyCondition, Status: metav1.ConditionUnknown}, + {Type: v1alpha1.PausedCondition, Status: metav1.ConditionTrue}, + }, + }, + } + newIntf := oldIntf.DeepCopy() + + e := event.UpdateEvent{ObjectOld: oldIntf, ObjectNew: newIntf} + Expect(p.Update(e)).To(BeTrue()) + }) + }) }) diff --git a/internal/controller/core/isis_controller.go b/internal/controller/core/isis_controller.go index f08ec05ea..5c361a473 100644 --- a/internal/controller/core/isis_controller.go +++ b/internal/controller/core/isis_controller.go @@ -380,7 +380,8 @@ func (r *ISISReconciler) interfaceToISIS(ctx context.Context, obj client.Object) log := ctrl.LoggerFrom(ctx, "Interface", klog.KObj(iface)) list := new(v1alpha1.ISISList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(iface.Namespace), ); err != nil { log.Error(err, "Failed to list ISISs") @@ -416,7 +417,8 @@ func (r *ISISReconciler) deviceToISISs(ctx context.Context, obj client.Object) [ log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(v1alpha1.ISISList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { diff --git a/internal/controller/core/lldp_controller.go b/internal/controller/core/lldp_controller.go index 0e6a91593..8ddf306a5 100644 --- a/internal/controller/core/lldp_controller.go +++ b/internal/controller/core/lldp_controller.go @@ -382,7 +382,8 @@ func (r *LLDPReconciler) reconcileInterfaceRef(ctx context.Context, interfaceRef func (r *LLDPReconciler) validateUniqueLLDPPerDevice(ctx context.Context, s *lldpScope) error { var list v1alpha1.LLDPList - if err := r.List(ctx, &list, + if err := r.List( + ctx, &list, client.InNamespace(s.LLDP.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: s.Device.Name}, ); err != nil { @@ -433,6 +434,7 @@ func (r *LLDPReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) for _, gvk := range v1alpha1.LLDPDependencies { obj := &unstructured.Unstructured{} obj.SetGroupVersionKind(gvk) + c = c.Watches( obj, handler.EnqueueRequestsFromMapFunc(r.mapProviderConfigToLLDP), @@ -440,21 +442,35 @@ func (r *LLDPReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) ) } - // Watches enqueues LLDPs for updates in referenced Device resources. - // Triggers on create, delete, and update events when the device's effective pause state changes. - c = c.Watches( - &v1alpha1.Device{}, - handler.EnqueueRequestsFromMapFunc(r.deviceToLLDPs), - builder.WithPredicates(predicate.Funcs{ - UpdateFunc: func(e event.UpdateEvent) bool { - return paused.DevicePausedChanged(e.ObjectOld, e.ObjectNew) - }, - GenericFunc: func(e event.GenericEvent) bool { - return false - }, - }), - ) - + c = c. + // Triggers on create, delete, and update events when the device's effective pause state changes. + // Watches enqueues LLDPs for updates in referenced Device resources. + Watches( + &v1alpha1.Device{}, + handler.EnqueueRequestsFromMapFunc(r.deviceToLLDPs), + builder.WithPredicates(predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { + return paused.DevicePausedChanged(e.ObjectOld, e.ObjectNew) + }, + GenericFunc: func(e event.GenericEvent) bool { + return false + }, + }), + ). + // Watches enqueues LLDPs for updates in referenced Interface resources. + // This ensures LLDP reconciles when a referenced Interface is created or updated. + Watches( + &v1alpha1.Interface{}, + handler.EnqueueRequestsFromMapFunc(r.interfaceToLLDPs), + builder.WithPredicates(predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { + return false + }, + GenericFunc: func(e event.GenericEvent) bool { + return false + }, + }), + ) return c.Complete(r) } @@ -513,7 +529,8 @@ func (r *LLDPReconciler) deviceToLLDPs(ctx context.Context, obj client.Object) [ log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) lldps := new(v1alpha1.LLDPList) - if err := r.List(ctx, lldps, + if err := r.List( + ctx, lldps, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { @@ -534,3 +551,42 @@ func (r *LLDPReconciler) deviceToLLDPs(ctx context.Context, obj client.Object) [ return requests } + +// interfaceToLLDPs is a [handler.MapFunc] to be used to enqueue requests for reconciliation +// for LLDPs when a referenced Interface is created or updated. +func (r *LLDPReconciler) interfaceToLLDPs(ctx context.Context, obj client.Object) []ctrl.Request { + intf, ok := obj.(*v1alpha1.Interface) + if !ok { + panic(fmt.Sprintf("Expected an Interface but got a %T", obj)) + } + + log := ctrl.LoggerFrom(ctx, "Interface", klog.KObj(intf)) + + list := new(v1alpha1.LLDPList) + if err := r.List( + ctx, list, + client.InNamespace(intf.Namespace), + client.MatchingFields{v1alpha1.DeviceRefIndexKey: intf.Spec.DeviceRef.Name}, + ); err != nil { + log.Error(err, "Failed to list LLDPs") + return nil + } + + var requests []ctrl.Request + for _, lldp := range list.Items { + for _, ifRef := range lldp.Spec.InterfaceRefs { + if ifRef.Name == intf.Name { + log.V(2).Info("Enqueuing LLDP for reconciliation", "LLDP", klog.KObj(&lldp)) + requests = append(requests, ctrl.Request{ + NamespacedName: client.ObjectKey{ + Name: lldp.Name, + Namespace: lldp.Namespace, + }, + }) + break + } + } + } + + return requests +} diff --git a/internal/controller/core/lldp_controller_test.go b/internal/controller/core/lldp_controller_test.go index ea58dabde..f47e833da 100644 --- a/internal/controller/core/lldp_controller_test.go +++ b/internal/controller/core/lldp_controller_test.go @@ -1057,4 +1057,356 @@ var _ = Describe("LLDP Controller", func() { Expect(k8sClient.Delete(ctx, intf)).To(Succeed()) }) }) + + Context("When Interface is created after LLDP", func() { + var ( + deviceName string + interfaceName string + resourceKey client.ObjectKey + deviceKey client.ObjectKey + interfaceKey client.ObjectKey + device *v1alpha1.Device + lldp *v1alpha1.LLDP + intf *v1alpha1.Interface + ) + + BeforeEach(func() { + By("Creating the Device resource") + device = &v1alpha1.Device{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "testlldp-watch-device-", + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.DeviceSpec{ + Endpoint: v1alpha1.Endpoint{ + Address: "192.168.10.7:9339", + }, + }, + } + Expect(k8sClient.Create(ctx, device)).To(Succeed()) + deviceName = device.Name + deviceKey = client.ObjectKey{Name: deviceName, Namespace: metav1.NamespaceDefault} + resourceKey = client.ObjectKey{Name: deviceName + "-lldp", Namespace: metav1.NamespaceDefault} + interfaceName = deviceName + "-intf" + interfaceKey = client.ObjectKey{Name: interfaceName, Namespace: metav1.NamespaceDefault} + }) + + AfterEach(func() { + By("Cleaning up the LLDP resource") + lldp = &v1alpha1.LLDP{} + err := k8sClient.Get(ctx, resourceKey, lldp) + if err == nil { + Expect(k8sClient.Delete(ctx, lldp)).To(Succeed()) + + By("Waiting for LLDP resource to be fully deleted") + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, resourceKey, &v1alpha1.LLDP{}) + g.Expect(errors.IsNotFound(err)).To(BeTrue()) + }).Should(Succeed()) + } + + By("Cleaning up the Interface resource") + intf = &v1alpha1.Interface{} + err = k8sClient.Get(ctx, interfaceKey, intf) + if err == nil { + Expect(k8sClient.Delete(ctx, intf)).To(Succeed()) + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, interfaceKey, &v1alpha1.Interface{}) + g.Expect(errors.IsNotFound(err)).To(BeTrue()) + }).Should(Succeed()) + } + + By("Cleaning up the Device resource") + err = k8sClient.Get(ctx, deviceKey, device) + if err == nil { + Expect(k8sClient.Delete(ctx, device, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) + } + + By("Verifying the provider has been cleaned up") + Eventually(func(g Gomega) { + g.Expect(testProvider.LLDP).To(BeNil(), "Provider should have no LLDP configured") + }).Should(Succeed()) + }) + + It("Should re-reconcile LLDP when referenced Interface is created", func() { + By("Creating LLDP with InterfaceRef to non-existent Interface") + lldp = &v1alpha1.LLDP{ + ObjectMeta: metav1.ObjectMeta{ + Name: deviceName + "-lldp", + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.LLDPSpec{ + DeviceRef: v1alpha1.LocalObjectReference{Name: deviceName}, + AdminState: v1alpha1.AdminStateUp, + InterfaceRefs: []v1alpha1.LLDPInterface{{ + LocalObjectReference: v1alpha1.LocalObjectReference{Name: interfaceName}, + AdminState: v1alpha1.AdminStateUp, + }}, + }, + } + Expect(k8sClient.Create(ctx, lldp)).To(Succeed()) + + By("Verifying LLDP is not ready due to missing Interface") + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, resourceKey, lldp) + g.Expect(err).NotTo(HaveOccurred()) + + cond := meta.FindStatusCondition(lldp.Status.Conditions, v1alpha1.ConfiguredCondition) + g.Expect(cond).ToNot(BeNil()) + g.Expect(cond.Status).To(Equal(metav1.ConditionFalse)) + g.Expect(cond.Reason).To(Equal(v1alpha1.WaitingForDependenciesReason)) + }).Should(Succeed()) + + By("Creating the referenced Interface") + intf = &v1alpha1.Interface{ + ObjectMeta: metav1.ObjectMeta{ + Name: interfaceName, + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.InterfaceSpec{ + DeviceRef: v1alpha1.LocalObjectReference{Name: deviceName}, + Name: "Ethernet1/1", + Type: v1alpha1.InterfaceTypePhysical, + AdminState: v1alpha1.AdminStateUp, + }, + } + Expect(k8sClient.Create(ctx, intf)).To(Succeed()) + + By("Verifying LLDP becomes ready after Interface is created (watch triggered re-reconciliation)") + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, resourceKey, lldp) + g.Expect(err).NotTo(HaveOccurred()) + + cond := meta.FindStatusCondition(lldp.Status.Conditions, v1alpha1.ReadyCondition) + g.Expect(cond).ToNot(BeNil()) + g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + + cond = meta.FindStatusCondition(lldp.Status.Conditions, v1alpha1.ConfiguredCondition) + g.Expect(cond).ToNot(BeNil()) + g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + }).Should(Succeed()) + }) + + It("Should re-reconcile LLDP when referenced Interface is deleted", func() { + By("Creating the Interface first") + intf = &v1alpha1.Interface{ + ObjectMeta: metav1.ObjectMeta{ + Name: interfaceName, + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.InterfaceSpec{ + DeviceRef: v1alpha1.LocalObjectReference{Name: deviceName}, + Name: "Ethernet1/1", + Type: v1alpha1.InterfaceTypePhysical, + AdminState: v1alpha1.AdminStateUp, + }, + } + Expect(k8sClient.Create(ctx, intf)).To(Succeed()) + + By("Creating LLDP with InterfaceRef") + lldp = &v1alpha1.LLDP{ + ObjectMeta: metav1.ObjectMeta{ + Name: deviceName + "-lldp", + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.LLDPSpec{ + DeviceRef: v1alpha1.LocalObjectReference{Name: deviceName}, + AdminState: v1alpha1.AdminStateUp, + InterfaceRefs: []v1alpha1.LLDPInterface{{ + LocalObjectReference: v1alpha1.LocalObjectReference{Name: interfaceName}, + AdminState: v1alpha1.AdminStateUp, + }}, + }, + } + Expect(k8sClient.Create(ctx, lldp)).To(Succeed()) + + By("Verifying LLDP is ready") + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, resourceKey, lldp) + g.Expect(err).NotTo(HaveOccurred()) + + cond := meta.FindStatusCondition(lldp.Status.Conditions, v1alpha1.ReadyCondition) + g.Expect(cond).ToNot(BeNil()) + g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + }).Should(Succeed()) + + By("Deleting the referenced Interface") + Expect(k8sClient.Delete(ctx, intf)).To(Succeed()) + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, interfaceKey, &v1alpha1.Interface{}) + g.Expect(errors.IsNotFound(err)).To(BeTrue()) + }).Should(Succeed()) + // Clear intf so AfterEach doesn't try to delete it again + intf = nil + + By("Verifying LLDP becomes not-ready after Interface is deleted (watch triggered re-reconciliation)") + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, resourceKey, lldp) + g.Expect(err).NotTo(HaveOccurred()) + + cond := meta.FindStatusCondition(lldp.Status.Conditions, v1alpha1.ConfiguredCondition) + g.Expect(cond).ToNot(BeNil()) + g.Expect(cond.Status).To(Equal(metav1.ConditionFalse)) + g.Expect(cond.Reason).To(Equal(v1alpha1.WaitingForDependenciesReason)) + }).Should(Succeed()) + }) + }) + + Context("When LLDP operational status is degraded", func() { + var ( + deviceName string + resourceKey client.ObjectKey + deviceKey client.ObjectKey + device *v1alpha1.Device + lldp *v1alpha1.LLDP + ) + + BeforeEach(func() { + By("Creating the Device resource") + device = &v1alpha1.Device{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "testlldp-oper-device-", + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.DeviceSpec{ + Endpoint: v1alpha1.Endpoint{ + Address: "192.168.10.8:9339", + }, + }, + } + Expect(k8sClient.Create(ctx, device)).To(Succeed()) + deviceName = device.Name + deviceKey = client.ObjectKey{Name: deviceName, Namespace: metav1.NamespaceDefault} + resourceKey = client.ObjectKey{Name: deviceName + "-lldp", Namespace: metav1.NamespaceDefault} + }) + + AfterEach(func() { + By("Resetting provider LLDP operational status to true") + testProvider.Lock() + testProvider.LLDPOperStatus = true + testProvider.Unlock() + + By("Cleaning up the LLDP resource") + lldp = &v1alpha1.LLDP{} + err := k8sClient.Get(ctx, resourceKey, lldp) + if err == nil { + Expect(k8sClient.Delete(ctx, lldp)).To(Succeed()) + + By("Waiting for LLDP resource to be fully deleted") + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, resourceKey, &v1alpha1.LLDP{}) + g.Expect(errors.IsNotFound(err)).To(BeTrue()) + }).Should(Succeed()) + } + + By("Cleaning up the Device resource") + err = k8sClient.Get(ctx, deviceKey, device) + if err == nil { + Expect(k8sClient.Delete(ctx, device, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) + } + + By("Verifying the provider has been cleaned up") + Eventually(func(g Gomega) { + g.Expect(testProvider.LLDP).To(BeNil(), "Provider should have no LLDP configured") + }).Should(Succeed()) + }) + + It("Should set OperationalCondition to False when LLDP is operationally down", func() { + By("Setting provider to return operational status down") + testProvider.Lock() + testProvider.LLDPOperStatus = false + testProvider.Unlock() + + By("Creating LLDP resource") + lldp = &v1alpha1.LLDP{ + ObjectMeta: metav1.ObjectMeta{ + Name: deviceName + "-lldp", + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.LLDPSpec{ + DeviceRef: v1alpha1.LocalObjectReference{Name: deviceName}, + AdminState: v1alpha1.AdminStateUp, + }, + } + Expect(k8sClient.Create(ctx, lldp)).To(Succeed()) + + By("Verifying OperationalCondition is False with DegradedReason") + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, resourceKey, lldp) + g.Expect(err).NotTo(HaveOccurred()) + + cond := meta.FindStatusCondition(lldp.Status.Conditions, v1alpha1.OperationalCondition) + g.Expect(cond).ToNot(BeNil()) + g.Expect(cond.Status).To(Equal(metav1.ConditionFalse)) + g.Expect(cond.Reason).To(Equal(v1alpha1.DegradedReason)) + g.Expect(cond.Message).To(ContainSubstring("operationally down")) + }).Should(Succeed()) + + By("Verifying ReadyCondition is also False due to degraded operational status") + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, resourceKey, lldp) + g.Expect(err).NotTo(HaveOccurred()) + + cond := meta.FindStatusCondition(lldp.Status.Conditions, v1alpha1.ReadyCondition) + g.Expect(cond).ToNot(BeNil()) + g.Expect(cond.Status).To(Equal(metav1.ConditionFalse)) + }).Should(Succeed()) + }) + + It("Should recover when LLDP becomes operationally up", func() { + By("Setting provider to return operational status down") + testProvider.Lock() + testProvider.LLDPOperStatus = false + testProvider.Unlock() + + By("Creating LLDP resource") + lldp = &v1alpha1.LLDP{ + ObjectMeta: metav1.ObjectMeta{ + Name: deviceName + "-lldp", + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.LLDPSpec{ + DeviceRef: v1alpha1.LocalObjectReference{Name: deviceName}, + AdminState: v1alpha1.AdminStateUp, + }, + } + Expect(k8sClient.Create(ctx, lldp)).To(Succeed()) + + By("Verifying OperationalCondition is False") + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, resourceKey, lldp) + g.Expect(err).NotTo(HaveOccurred()) + + cond := meta.FindStatusCondition(lldp.Status.Conditions, v1alpha1.OperationalCondition) + g.Expect(cond).ToNot(BeNil()) + g.Expect(cond.Status).To(Equal(metav1.ConditionFalse)) + }).Should(Succeed()) + + By("Setting provider to return operational status up") + testProvider.Lock() + testProvider.LLDPOperStatus = true + testProvider.Unlock() + + By("Verifying OperationalCondition becomes True after requeue") + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, resourceKey, lldp) + g.Expect(err).NotTo(HaveOccurred()) + + cond := meta.FindStatusCondition(lldp.Status.Conditions, v1alpha1.OperationalCondition) + g.Expect(cond).ToNot(BeNil()) + g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(cond.Reason).To(Equal(v1alpha1.OperationalReason)) + }).Should(Succeed()) + + By("Verifying ReadyCondition is also True") + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, resourceKey, lldp) + g.Expect(err).NotTo(HaveOccurred()) + + cond := meta.FindStatusCondition(lldp.Status.Conditions, v1alpha1.ReadyCondition) + g.Expect(cond).ToNot(BeNil()) + g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + }).Should(Succeed()) + }) + }) }) diff --git a/internal/controller/core/managementaccess_controller.go b/internal/controller/core/managementaccess_controller.go index 7e3bd6d09..a715739ec 100644 --- a/internal/controller/core/managementaccess_controller.go +++ b/internal/controller/core/managementaccess_controller.go @@ -324,7 +324,8 @@ func (r *ManagementAccessReconciler) deviceToManagementAccesses(ctx context.Cont log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(v1alpha1.ManagementAccessList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { diff --git a/internal/controller/core/ntp_controller.go b/internal/controller/core/ntp_controller.go index c99f2148d..c5adb2706 100644 --- a/internal/controller/core/ntp_controller.go +++ b/internal/controller/core/ntp_controller.go @@ -324,7 +324,8 @@ func (r *NTPReconciler) deviceToNTPs(ctx context.Context, obj client.Object) []c log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(v1alpha1.NTPList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { diff --git a/internal/controller/core/nve_controller.go b/internal/controller/core/nve_controller.go index 396877c96..09b273522 100644 --- a/internal/controller/core/nve_controller.go +++ b/internal/controller/core/nve_controller.go @@ -300,7 +300,8 @@ func (r *NetworkVirtualizationEdgeReconciler) reconcile(ctx context.Context, s * func (r *NetworkVirtualizationEdgeReconciler) validateUniqueNVEPerDevice(ctx context.Context, s *nveScope) error { var list v1alpha1.NetworkVirtualizationEdgeList - if err := r.List(ctx, &list, + if err := r.List( + ctx, &list, client.InNamespace(s.NVE.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: s.NVE.Spec.DeviceRef.Name}, ); err != nil { @@ -520,7 +521,8 @@ func (r *NetworkVirtualizationEdgeReconciler) deviceToNVEs(ctx context.Context, log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(v1alpha1.NetworkVirtualizationEdgeList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { diff --git a/internal/controller/core/ospf_controller.go b/internal/controller/core/ospf_controller.go index 0f8a8124b..7fbe57786 100644 --- a/internal/controller/core/ospf_controller.go +++ b/internal/controller/core/ospf_controller.go @@ -464,7 +464,8 @@ func (r *OSPFReconciler) deviceToOSPFs(ctx context.Context, obj client.Object) [ log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(v1alpha1.OSPFList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { @@ -497,7 +498,8 @@ func (r *OSPFReconciler) interfaceToOSPF(ctx context.Context, obj client.Object) log := ctrl.LoggerFrom(ctx, "Interface", klog.KObj(iface)) list := new(v1alpha1.OSPFList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(iface.Namespace), ); err != nil { log.Error(err, "Failed to list OSPFs") diff --git a/internal/controller/core/pim_controller.go b/internal/controller/core/pim_controller.go index ba407c65c..8fef7afe4 100644 --- a/internal/controller/core/pim_controller.go +++ b/internal/controller/core/pim_controller.go @@ -382,7 +382,8 @@ func (r *PIMReconciler) deviceToPIMs(ctx context.Context, obj client.Object) []c log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(v1alpha1.PIMList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { @@ -415,7 +416,8 @@ func (r *PIMReconciler) interfaceToPIM(ctx context.Context, obj client.Object) [ log := ctrl.LoggerFrom(ctx, "Interface", klog.KObj(iface)) list := new(v1alpha1.PIMList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(iface.Namespace), ); err != nil { log.Error(err, "Failed to list PIMs") diff --git a/internal/controller/core/prefixset_controller.go b/internal/controller/core/prefixset_controller.go index a90d8b5fe..c144af7b8 100644 --- a/internal/controller/core/prefixset_controller.go +++ b/internal/controller/core/prefixset_controller.go @@ -333,7 +333,8 @@ func (r *PrefixSetReconciler) deviceToPrefixSets(ctx context.Context, obj client log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(v1alpha1.PrefixSetList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { diff --git a/internal/controller/core/routingpolicy_controller.go b/internal/controller/core/routingpolicy_controller.go index 94678ca23..ad0c02d50 100644 --- a/internal/controller/core/routingpolicy_controller.go +++ b/internal/controller/core/routingpolicy_controller.go @@ -461,7 +461,8 @@ func (r *RoutingPolicyReconciler) deviceToRoutingPolicies(ctx context.Context, o log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(v1alpha1.RoutingPolicyList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { diff --git a/internal/controller/core/snmp_controller.go b/internal/controller/core/snmp_controller.go index 94dbc3496..af54b90e2 100644 --- a/internal/controller/core/snmp_controller.go +++ b/internal/controller/core/snmp_controller.go @@ -326,7 +326,8 @@ func (r *SNMPReconciler) deviceToSNMPs(ctx context.Context, obj client.Object) [ log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(v1alpha1.SNMPList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { diff --git a/internal/controller/core/suite_test.go b/internal/controller/core/suite_test.go index d505254d8..81065d523 100644 --- a/internal/controller/core/suite_test.go +++ b/internal/controller/core/suite_test.go @@ -441,6 +441,8 @@ type Provider struct { RoutingPolicies sets.Set[string] NVE *v1alpha1.NetworkVirtualizationEdge LLDP *v1alpha1.LLDP + LLDPOperStatus bool + LLDPNeighbors map[string]*provider.LLDPAdjacency DHCPRelay *v1alpha1.DHCPRelay } @@ -459,6 +461,8 @@ func NewProvider() *Provider { EVIs: sets.New[int32](), PrefixSets: sets.New[string](), RoutingPolicies: sets.New[string](), + LLDPOperStatus: true, + LLDPNeighbors: make(map[string]*provider.LLDPAdjacency), } } @@ -545,10 +549,23 @@ func (p *Provider) DeleteInterface(_ context.Context, req *provider.InterfaceReq return nil } -func (p *Provider) GetInterfaceStatus(context.Context, *provider.InterfaceRequest) (provider.InterfaceStatus, error) { - return provider.InterfaceStatus{ +func (p *Provider) GetInterfaceStatus(_ context.Context, req *provider.InterfaceRequest) (provider.InterfaceStatus, error) { + p.Lock() + defer p.Unlock() + + status := provider.InterfaceStatus{ OperStatus: true, - }, nil + } + + if neighbor, ok := p.LLDPNeighbors[req.Interface.Spec.Name]; ok { + status.LLDPAdjacencies = []provider.LLDPAdjacency{*neighbor} + } + + return status, nil +} + +func (p *Provider) InterfaceNameEqual(_ context.Context, a, b string) (bool, error) { + return a == b, nil } func (p *Provider) EnsureBanner(_ context.Context, req *provider.EnsureBannerRequest) error { @@ -903,7 +920,9 @@ func (p *Provider) DeleteLLDP(_ context.Context, req *provider.LLDPRequest) erro } func (p *Provider) GetLLDPStatus(_ context.Context, _ *provider.LLDPRequest) (provider.LLDPStatus, error) { - return provider.LLDPStatus{OperStatus: true}, nil + p.Lock() + defer p.Unlock() + return provider.LLDPStatus{OperStatus: p.LLDPOperStatus}, nil } func (p *Provider) EnsureDHCPRelay(_ context.Context, req *provider.DHCPRelayRequest) error { @@ -932,3 +951,15 @@ func (p *Provider) GetDHCPRelayStatus(_ context.Context, req *provider.DHCPRelay } return status, nil } + +// SetLLDPNeighbor is a test helper to configure LLDP neighbor information for an interface. +func (p *Provider) SetLLDPNeighbor(interfaceName, sysName, chassisID, portID string, ttl uint32) { + p.Lock() + defer p.Unlock() + p.LLDPNeighbors[interfaceName] = &provider.LLDPAdjacency{ + SysName: sysName, + ChassisID: chassisID, + PortID: portID, + TTL: time.Duration(ttl) * time.Second, + } +} diff --git a/internal/controller/core/syslog_controller.go b/internal/controller/core/syslog_controller.go index 1e6e8a13e..00c754b96 100644 --- a/internal/controller/core/syslog_controller.go +++ b/internal/controller/core/syslog_controller.go @@ -330,7 +330,8 @@ func (r *SyslogReconciler) deviceToSyslogs(ctx context.Context, obj client.Objec log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(v1alpha1.SyslogList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { diff --git a/internal/controller/core/user_controller.go b/internal/controller/core/user_controller.go index 9690d2ee1..edb776b34 100644 --- a/internal/controller/core/user_controller.go +++ b/internal/controller/core/user_controller.go @@ -402,7 +402,8 @@ func (r *UserReconciler) deviceToUsers(ctx context.Context, obj client.Object) [ log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(v1alpha1.UserList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { diff --git a/internal/controller/core/vlan_controller.go b/internal/controller/core/vlan_controller.go index e6e6ad7c7..06d4aab85 100644 --- a/internal/controller/core/vlan_controller.go +++ b/internal/controller/core/vlan_controller.go @@ -358,7 +358,8 @@ func (r *VLANReconciler) deviceToVLANs(ctx context.Context, obj client.Object) [ log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(v1alpha1.VLANList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { diff --git a/internal/controller/core/vrf_controller.go b/internal/controller/core/vrf_controller.go index 16bdd5e24..dd51c522d 100644 --- a/internal/controller/core/vrf_controller.go +++ b/internal/controller/core/vrf_controller.go @@ -329,7 +329,8 @@ func (r *VRFReconciler) deviceToVRFs(ctx context.Context, obj client.Object) []c log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) list := new(v1alpha1.VRFList) - if err := r.List(ctx, list, + if err := r.List( + ctx, list, client.InNamespace(device.Namespace), client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, ); err != nil { diff --git a/internal/provider/cisco/iosxr/provider.go b/internal/provider/cisco/iosxr/provider.go index 09d785c59..a8988e5ce 100644 --- a/internal/provider/cisco/iosxr/provider.go +++ b/internal/provider/cisco/iosxr/provider.go @@ -297,6 +297,11 @@ func (p *Provider) GetInterfaceStatus(ctx context.Context, req *provider.Interfa }, nil } +func (p *Provider) InterfaceNameEqual(_ context.Context, a, b string) (bool, error) { + // TODO: implement provider specific logic to compare interface names + return a == b, nil +} + func init() { provider.Register("cisco-iosxr-gnmi", NewProvider) } diff --git a/internal/provider/cisco/nxos/lldp.go b/internal/provider/cisco/nxos/lldp.go index a42875729..efbe17d51 100644 --- a/internal/provider/cisco/nxos/lldp.go +++ b/internal/provider/cisco/nxos/lldp.go @@ -41,3 +41,27 @@ func (*LLDPOper) IsListItem() {} func (*LLDPOper) XPath() string { return "System/fm-items/lldp-items" } + +// LLDPAdjacencyItems represents the LLDP neighbor information for a single interface. +type LLDPAdjacencyItems struct { + // ID is the identifier of the interface for which the LLDP neighbor information is being retrieved, e.g., "eth1/1". + ID string `json:"-"` + AdjItems struct { + AdjEpList []struct { + ChassisIDT uint8 `json:"chassisIdT"` + ChassisIDV string `json:"chassisIdV"` + PortIDT uint8 `json:"portIdT"` + PortIDV string `json:"portIdV"` + PortDesc string `json:"portDesc,omitempty"` + SysName string `json:"sysName,omitempty"` + SysDesc string `json:"sysDesc,omitempty"` + TTL int32 `json:"ttl"` + } `json:"AdjEp-list,omitzero"` + } `json:"adj-items,omitzero"` +} + +func (p *LLDPAdjacencyItems) XPath() string { + return "System/lldp-items/inst-items/if-items/If-list[id=" + p.ID + "]" +} + +func (*LLDPAdjacencyItems) IsListItem() {} diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index 502d77478..d270637cb 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -193,15 +193,22 @@ func (p *Provider) ListPorts(ctx context.Context) ([]provider.DevicePort, error) } func (p *Provider) GetDeviceInfo(ctx context.Context) (*provider.DeviceInfo, error) { + h := new(Hostname) m := new(Model) s := new(SerialNumber) fw := new(FirmwareVersion) + + // Hostname is a config item, not state + if err := p.client.GetConfig(ctx, h); err != nil { + return nil, err + } if err := p.client.GetState(ctx, m, s, fw); err != nil { return nil, err } return &provider.DeviceInfo{ Manufacturer: Manufacturer, + Hostname: string(*h), Model: string(*m), SerialNumber: string(*s), FirmwareVersion: string(*fw), @@ -1325,19 +1332,37 @@ func (p *Provider) GetInterfaceStatus(ctx context.Context, req *provider.Interfa } var ( - operSt OperSt - operMsg string + operSt OperSt + operMsg string + lldpAdjacencies []provider.LLDPAdjacency ) switch req.Interface.Spec.Type { case v1alpha1.InterfaceTypePhysical: phys := new(PhysIfOperItems) phys.ID = name - if err := p.client.GetState(ctx, phys); err != nil && !errors.Is(err, gnmiext.ErrNil) { + lldpAdj := new(LLDPAdjacencyItems) + lldpAdj.ID = name + if err := p.client.GetState(ctx, phys, lldpAdj); err != nil && !errors.Is(err, gnmiext.ErrNil) { return provider.InterfaceStatus{}, err } operSt = phys.OperSt operMsg = phys.OperStQual + lldpAdjacencies = make([]provider.LLDPAdjacency, 0, len(lldpAdj.AdjItems.AdjEpList)) + for _, adj := range lldpAdj.AdjItems.AdjEpList { + neighbor := provider.LLDPAdjacency{ + ChassisID: adj.ChassisIDV, + ChassisIDType: adj.ChassisIDT, + PortID: adj.PortIDV, + PortIDType: adj.PortIDT, + PortDescription: adj.PortDesc, + SysName: adj.SysName, + SysDescription: adj.SysDesc, + TTL: time.Duration(adj.TTL) * time.Second, + } + lldpAdjacencies = append(lldpAdjacencies, neighbor) + } + case v1alpha1.InterfaceTypeLoopback: lb := new(LoopbackOperItems) lb.ID = name @@ -1385,10 +1410,25 @@ func (p *Provider) GetInterfaceStatus(ctx context.Context, req *provider.Interfa operMsg = "" } - return provider.InterfaceStatus{ - OperStatus: operSt == OperStUp, - OperMessage: operMsg, - }, nil + status := provider.InterfaceStatus{ + OperStatus: operSt == OperStUp, + OperMessage: operMsg, + LLDPAdjacencies: lldpAdjacencies, + } + + return status, nil +} + +func (p *Provider) InterfaceNameEqual(_ context.Context, a, b string) (bool, error) { + shortA, err := ShortName(a) + if err != nil { + return false, fmt.Errorf("invalid interface name: %w", err) + } + shortB, err := ShortName(b) + if err != nil { + return false, fmt.Errorf("invalid interface name: %w", err) + } + return shortA == shortB, nil } var ErrInterfaceNotFound = errors.New("one or more interfaces do not exist") diff --git a/internal/provider/cisco/nxos/system.go b/internal/provider/cisco/nxos/system.go index 5b4acb950..8d8fe2b57 100644 --- a/internal/provider/cisco/nxos/system.go +++ b/internal/provider/cisco/nxos/system.go @@ -39,6 +39,13 @@ func (s *SystemJumboMTU) Default() { *s = 9216 } +// Hostname is the configured hostname of the device. +type Hostname string + +func (*Hostname) XPath() string { + return "System/name" +} + // Model is the chassis model of the device, e.g. "N9K-C9336C-FX2". type Model string diff --git a/internal/provider/openconfig/provider.go b/internal/provider/openconfig/provider.go index 96437a9b6..424d2e690 100644 --- a/internal/provider/openconfig/provider.go +++ b/internal/provider/openconfig/provider.go @@ -200,6 +200,11 @@ func (p *Provider) GetInterfaceStatus(context.Context, *provider.InterfaceReques return provider.InterfaceStatus{}, nil } +func (p *Provider) InterfaceNameEqual(_ context.Context, a, b string) (bool, error) { + // TODO: implement provider specific logic to compare interface names + return a == b, nil +} + func init() { provider.Register("openconfig", NewProvider) } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 50005701b..f1845132e 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -65,6 +65,8 @@ type DevicePort struct { } type DeviceInfo struct { + // Hostname is the hostname of the device. + Hostname string // Manufacturer is the manufacturer of the device, e.g. "Cisco". Manufacturer string // Model is the model of the device, e.g. "N9K-C9332D-GX2B". @@ -85,6 +87,8 @@ type InterfaceProvider interface { DeleteInterface(context.Context, *InterfaceRequest) error // GetInterfaceStatus call is responsible for retrieving the current status of the Interface from the provider. GetInterfaceStatus(context.Context, *InterfaceRequest) (InterfaceStatus, error) + // InterfaceNameEqual reports whether two interface names refer to the same interface on the provider. + InterfaceNameEqual(context.Context, string, string) (bool, error) } type EnsureInterfaceRequest struct { @@ -135,6 +139,20 @@ type InterfaceStatus struct { // OperMessage provides additional information about the operational status of the interface. // Leave empty if the provider does not return any additional information. OperMessage string + // LLDPAdjacencies provides information about the directly connected neighbors on this interface, if available. + LLDPAdjacencies []LLDPAdjacency +} + +// LLDPAdjacency represents information about a directly connected neighbor on an interface, as discovered through LLDP. +type LLDPAdjacency struct { + SysName string + SysDescription string + ChassisID string + ChassisIDType uint8 + PortID string + PortIDType uint8 + PortDescription string + TTL time.Duration } // BannerProvider is the interface for the realization of the Banner objects over different providers. diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 788531eba..032db0663 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -39,7 +39,8 @@ var _ = Describe("Manager", Ordered, func() { // and deploying the controller. BeforeAll(func(ctx SpecContext) { By("deploying the gnmi-test-server") - cmd := exec.CommandContext(ctx, "kubectl", "run", "gnmi-test-server", + cmd := exec.CommandContext( + ctx, "kubectl", "run", "gnmi-test-server", "--image", serverImage, "--image-pull-policy", "Never", "--namespace", "default", @@ -50,7 +51,8 @@ var _ = Describe("Manager", Ordered, func() { _, err := Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to deploy the gnmi-test-server") - cmd = exec.CommandContext(ctx, "kubectl", "wait", "pods/gnmi-test-server", + cmd = exec.CommandContext( + ctx, "kubectl", "wait", "pods/gnmi-test-server", "--for", "condition=Ready", "--namespace", "default", "--timeout", "1m", @@ -58,7 +60,8 @@ var _ = Describe("Manager", Ordered, func() { _, err = Run(cmd) Expect(err).NotTo(HaveOccurred()) - cmd = exec.CommandContext(ctx, "kubectl", "get", "pod", "gnmi-test-server", + cmd = exec.CommandContext( + ctx, "kubectl", "get", "pod", "gnmi-test-server", "--output", "jsonpath='{.status.podIP}'", "--namespace", "default", ) @@ -171,7 +174,8 @@ var _ = Describe("Manager", Ordered, func() { By("validating that the controller-manager pod is running as expected") verifyControllerUp := func(g Gomega) { // Get the name of the controller-manager pod - cmd := exec.CommandContext(ctx, "kubectl", "get", + cmd := exec.CommandContext( + ctx, "kubectl", "get", "pods", "-l", "control-plane=controller-manager", "-o", "go-template={{ range .items }}"+ "{{ if not .metadata.deletionTimestamp }}"+ @@ -316,7 +320,8 @@ var _ = Describe("Manager", Ordered, func() { // strings.ToLower(), // )) - DescribeTable("Should reconcile the api objects", + DescribeTable( + "Should reconcile the api objects", func(ctx SpecContext, file string) { device := ` apiVersion: networking.metal.ironcore.dev/v1alpha1 @@ -341,7 +346,8 @@ spec: Expect(err).NotTo(HaveOccurred(), "Failed to apply Interface") // #nosec G204 - cmd := exec.CommandContext(ctx, "kubectl", "wait", a.Files[0].Name, + cmd := exec.CommandContext( + ctx, "kubectl", "wait", a.Files[0].Name, "--for", "condition=Configured", "--namespace", "default", "--timeout", "5m", @@ -349,7 +355,8 @@ spec: _, err = Run(cmd) Expect(err).NotTo(HaveOccurred()) - cmd = exec.CommandContext(ctx, "kubectl", "exec", "gnmi-test-server", + cmd = exec.CommandContext( + ctx, "kubectl", "exec", "gnmi-test-server", "--namespace", "default", "--", "wget", "-qO-", "http://localhost:8000/v1/state", @@ -369,7 +376,8 @@ spec: _, err = Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to delete object") - cmd = exec.CommandContext(ctx, "kubectl", "exec", "gnmi-test-server", + cmd = exec.CommandContext( + ctx, "kubectl", "exec", "gnmi-test-server", "--namespace", "default", "--", "wget", "-qO-", "--header='X-HTTP-Method-Override: DELETE'", "http://localhost:8000/v1/state", diff --git a/test/e2e/util_test.go b/test/e2e/util_test.go index f4ac6fb3e..1281d18db 100644 --- a/test/e2e/util_test.go +++ b/test/e2e/util_test.go @@ -140,7 +140,8 @@ func InstallCertManager(ctx context.Context) error { } // Wait for cert-manager-webhook to be ready, which can take time if cert-manager // was re-installed after uninstalling on a cluster. - cmd = exec.CommandContext(ctx, "kubectl", "wait", "deployment.apps/cert-manager-webhook", + cmd = exec.CommandContext( + ctx, "kubectl", "wait", "deployment.apps/cert-manager-webhook", "--for", "condition=Available", "--namespace", "cert-manager", "--timeout", "5m", diff --git a/test/lab/main_test.go b/test/lab/main_test.go index 2e1d6a638..6ebb7f03d 100644 --- a/test/lab/main_test.go +++ b/test/lab/main_test.go @@ -97,7 +97,8 @@ func Vty() script.Cmd { stderr = stderrBuf.String() return }, nil - }) + }, + ) } // Apply returns a script command that applies a Kubernetes manifest to the cluster @@ -150,7 +151,8 @@ func Apply() script.Cmd { return "", "", fmt.Errorf("resource %s is not ready", res.GetName()) } return WaitTimeout(wait, timeout, interval), nil - }) + }, + ) } // TODO(felix-kaestner): Load endpoint configuration from a config file.