From fb3e9543a36757592b820c2ded1104751ca6818e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20K=C3=A4stner?= Date: Mon, 8 Dec 2025 15:59:18 +0100 Subject: [PATCH 1/5] Add bfd settings to `Interface` resource Assisted-by: Claude Code --- api/core/v1alpha1/interface_types.go | 40 +++++++++++++++++++ api/core/v1alpha1/zz_generated.deepcopy.go | 35 ++++++++++++++++ ...working.metal.ironcore.dev_interfaces.yaml | 40 +++++++++++++++++++ ...working.metal.ironcore.dev_interfaces.yaml | 40 +++++++++++++++++++ config/samples/v1alpha1_interface.yaml | 10 +++++ 5 files changed, 165 insertions(+) diff --git a/api/core/v1alpha1/interface_types.go b/api/core/v1alpha1/interface_types.go index 6a247597..336240fb 100644 --- a/api/core/v1alpha1/interface_types.go +++ b/api/core/v1alpha1/interface_types.go @@ -21,6 +21,8 @@ import ( // +kubebuilder:validation:XValidation:rule="self.type == 'RoutedVLAN' || !has(self.ipv4) || !self.ipv4.anycastGateway", message="anycastGateway can only be enabled for interfaces of type RoutedVLAN" // +kubebuilder:validation:XValidation:rule="self.type != 'Aggregate' || !has(self.vrfRef)", message="vrfRef must not be specified for interfaces of type Aggregate" // +kubebuilder:validation:XValidation:rule="self.type != 'Physical' || !has(self.switchport) || !has(self.vrfRef)", message="vrfRef must not be specified for Physical interfaces with switchport configuration" +// +kubebuilder:validation:XValidation:rule="self.type != 'Aggregate' || !has(self.bfd)", message="bfd must not be specified for interfaces of type Aggregate" +// +kubebuilder:validation:XValidation:rule="!has(self.bfd) || !has(self.switchport)", message="bfd must not be specified for interfaces with switchport configuration" type InterfaceSpec struct { // DeviceName is the name of the Device this object belongs to. The Device object must exist in the same namespace. // Immutable. @@ -86,6 +88,11 @@ type InterfaceSpec struct { // The referenced VRF must exist in the same namespace. // +optional VrfRef *LocalObjectReference `json:"vrfRef,omitempty"` + + // BFD defines the Bidirectional Forwarding Detection configuration for the interface. + // BFD is only applicable for Layer 3 interfaces (Physical, Loopback, RoutedVLAN). + // +optional + BFD *BFD `json:"bfd,omitempty"` } // AdminState represents the administrative state of the interface. @@ -192,6 +199,39 @@ type InterfaceIPv4Unnumbered struct { InterfaceRef LocalObjectReference `json:"interfaceRef"` } +// BFD defines the Bidirectional Forwarding Detection configuration for an interface. +type BFD struct { + // Enabled indicates whether BFD is enabled on the interface. + // +required + Enabled bool `json:"enabled"` + + // DesiredMinimumTxInterval is the minimum interval between transmission of BFD control + // packets that the operator desires. This value is advertised to the peer. + // The actual interval used is the maximum of this value and the remote + // required-minimum-receive interval value. + // +optional + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$" + DesiredMinimumTxInterval *metav1.Duration `json:"desiredMinimumTxInterval,omitempty"` + + // RequiredMinimumReceive is the minimum interval between received BFD control packets + // that this system should support. This value is advertised to the remote peer to + // indicate the maximum frequency between BFD control packets that is acceptable + // to the local system. + // +optional + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$" + RequiredMinimumReceive *metav1.Duration `json:"requiredMinimumReceive,omitempty"` + + // DetectionMultiplier is the number of packets that must be missed to declare + // this session as down. The detection interval for the BFD session is calculated + // by multiplying the value of the negotiated transmission interval by this value. + // +optional + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=255 + DetectionMultiplier *int32 `json:"detectionMultiplier,omitempty"` +} + type Aggregation struct { // MemberInterfaceRefs is a list of interface references that are part of the aggregate interface. // +required diff --git a/api/core/v1alpha1/zz_generated.deepcopy.go b/api/core/v1alpha1/zz_generated.deepcopy.go index 8e643614..59b1c1c5 100644 --- a/api/core/v1alpha1/zz_generated.deepcopy.go +++ b/api/core/v1alpha1/zz_generated.deepcopy.go @@ -179,6 +179,36 @@ func (in *Aggregation) DeepCopy() *Aggregation { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BFD) DeepCopyInto(out *BFD) { + *out = *in + if in.DesiredMinimumTxInterval != nil { + in, out := &in.DesiredMinimumTxInterval, &out.DesiredMinimumTxInterval + *out = new(v1.Duration) + **out = **in + } + if in.RequiredMinimumReceive != nil { + in, out := &in.RequiredMinimumReceive, &out.RequiredMinimumReceive + *out = new(v1.Duration) + **out = **in + } + if in.DetectionMultiplier != nil { + in, out := &in.DetectionMultiplier, &out.DetectionMultiplier + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BFD. +func (in *BFD) DeepCopy() *BFD { + if in == nil { + return nil + } + out := new(BFD) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BGP) DeepCopyInto(out *BGP) { *out = *in @@ -1631,6 +1661,11 @@ func (in *InterfaceSpec) DeepCopyInto(out *InterfaceSpec) { *out = new(LocalObjectReference) **out = **in } + if in.BFD != nil { + in, out := &in.BFD, &out.BFD + *out = new(BFD) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InterfaceSpec. diff --git a/charts/network-operator/templates/crd/networking.metal.ironcore.dev_interfaces.yaml b/charts/network-operator/templates/crd/networking.metal.ironcore.dev_interfaces.yaml index 677f5ae2..77274cf4 100644 --- a/charts/network-operator/templates/crd/networking.metal.ironcore.dev_interfaces.yaml +++ b/charts/network-operator/templates/crd/networking.metal.ironcore.dev_interfaces.yaml @@ -153,6 +153,42 @@ spec: required: - memberInterfaceRefs type: object + bfd: + description: |- + BFD defines the Bidirectional Forwarding Detection configuration for the interface. + BFD is only applicable for Layer 3 interfaces (Physical, Loopback, RoutedVLAN). + properties: + desiredMinimumTxInterval: + description: |- + DesiredMinimumTxInterval is the minimum interval between transmission of BFD control + packets that the operator desires. This value is advertised to the peer. + The actual interval used is the maximum of this value and the remote + required-minimum-receive interval value. + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + detectionMultiplier: + description: |- + DetectionMultiplier is the number of packets that must be missed to declare + this session as down. The detection interval for the BFD session is calculated + by multiplying the value of the negotiated transmission interval by this value. + format: int32 + maximum: 255 + minimum: 1 + type: integer + enabled: + description: Enabled indicates whether BFD is enabled on the interface. + type: boolean + requiredMinimumReceive: + description: |- + RequiredMinimumReceive is the minimum interval between received BFD control packets + that this system should support. This value is advertised to the remote peer to + indicate the maximum frequency between BFD control packets that is acceptable + to the local system. + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + required: + - enabled + type: object description: description: Description provides a human-readable description of the interface. @@ -405,6 +441,10 @@ spec: - message: vrfRef must not be specified for Physical interfaces with switchport configuration rule: self.type != 'Physical' || !has(self.switchport) || !has(self.vrfRef) + - message: bfd must not be specified for interfaces of type Aggregate + rule: self.type != 'Aggregate' || !has(self.bfd) + - message: bfd must not be specified for interfaces with switchport configuration + rule: '!has(self.bfd) || !has(self.switchport)' status: description: |- Status of the resource. This is set and updated automatically. diff --git a/config/crd/bases/networking.metal.ironcore.dev_interfaces.yaml b/config/crd/bases/networking.metal.ironcore.dev_interfaces.yaml index 9d56c81d..4292b3e2 100644 --- a/config/crd/bases/networking.metal.ironcore.dev_interfaces.yaml +++ b/config/crd/bases/networking.metal.ironcore.dev_interfaces.yaml @@ -147,6 +147,42 @@ spec: required: - memberInterfaceRefs type: object + bfd: + description: |- + BFD defines the Bidirectional Forwarding Detection configuration for the interface. + BFD is only applicable for Layer 3 interfaces (Physical, Loopback, RoutedVLAN). + properties: + desiredMinimumTxInterval: + description: |- + DesiredMinimumTxInterval is the minimum interval between transmission of BFD control + packets that the operator desires. This value is advertised to the peer. + The actual interval used is the maximum of this value and the remote + required-minimum-receive interval value. + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + detectionMultiplier: + description: |- + DetectionMultiplier is the number of packets that must be missed to declare + this session as down. The detection interval for the BFD session is calculated + by multiplying the value of the negotiated transmission interval by this value. + format: int32 + maximum: 255 + minimum: 1 + type: integer + enabled: + description: Enabled indicates whether BFD is enabled on the interface. + type: boolean + requiredMinimumReceive: + description: |- + RequiredMinimumReceive is the minimum interval between received BFD control packets + that this system should support. This value is advertised to the remote peer to + indicate the maximum frequency between BFD control packets that is acceptable + to the local system. + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + required: + - enabled + type: object description: description: Description provides a human-readable description of the interface. @@ -399,6 +435,10 @@ spec: - message: vrfRef must not be specified for Physical interfaces with switchport configuration rule: self.type != 'Physical' || !has(self.switchport) || !has(self.vrfRef) + - message: bfd must not be specified for interfaces of type Aggregate + rule: self.type != 'Aggregate' || !has(self.bfd) + - message: bfd must not be specified for interfaces with switchport configuration + rule: '!has(self.bfd) || !has(self.switchport)' status: description: |- Status of the resource. This is set and updated automatically. diff --git a/config/samples/v1alpha1_interface.yaml b/config/samples/v1alpha1_interface.yaml index 1bd062cf..4852ea7d 100644 --- a/config/samples/v1alpha1_interface.yaml +++ b/config/samples/v1alpha1_interface.yaml @@ -63,6 +63,11 @@ spec: unnumbered: interfaceRef: name: lo0 + bfd: + enabled: true + desiredMinimumTxInterval: 300ms + requiredMinimumReceive: 300ms + detectionMultiplier: 3 --- apiVersion: networking.metal.ironcore.dev/v1alpha1 kind: Interface @@ -84,6 +89,11 @@ spec: unnumbered: interfaceRef: name: lo0 + bfd: + enabled: true + desiredMinimumTxInterval: 500ms + requiredMinimumReceive: 500ms + detectionMultiplier: 5 --- apiVersion: networking.metal.ironcore.dev/v1alpha1 kind: Interface From 7295d1b385ed200d3bab9187ded22152b44eb916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20K=C3=A4stner?= Date: Fri, 12 Dec 2025 15:12:35 +0100 Subject: [PATCH 2/5] [NX-OS] Implement bfd interface settings --- internal/provider/cisco/nxos/intf.go | 31 +++++++++++++++++ internal/provider/cisco/nxos/intf_test.go | 6 ++++ internal/provider/cisco/nxos/provider.go | 33 +++++++++++++++++++ .../provider/cisco/nxos/testdata/bfd.json | 19 +++++++++++ .../provider/cisco/nxos/testdata/bfd.json.txt | 2 ++ 5 files changed, 91 insertions(+) create mode 100644 internal/provider/cisco/nxos/testdata/bfd.json create mode 100644 internal/provider/cisco/nxos/testdata/bfd.json.txt diff --git a/internal/provider/cisco/nxos/intf.go b/internal/provider/cisco/nxos/intf.go index e5a198e4..67fb7aee 100644 --- a/internal/provider/cisco/nxos/intf.go +++ b/internal/provider/cisco/nxos/intf.go @@ -24,6 +24,8 @@ var ( _ gnmiext.Configurable = (*PhysIfOperItems)(nil) _ gnmiext.Configurable = (*VrfMember)(nil) _ gnmiext.Configurable = (*SpanningTree)(nil) + _ gnmiext.Configurable = (*MultisiteIfTracking)(nil) + _ gnmiext.Configurable = (*BFD)(nil) _ gnmiext.Configurable = (*PortChannel)(nil) _ gnmiext.Configurable = (*PortChannelOperItems)(nil) _ gnmiext.Configurable = (*SwitchVirtualInterface)(nil) @@ -168,6 +170,35 @@ func (m *MultisiteIfTracking) XPath() string { return "System/intf-items/phys-items/PhysIf-list[id=" + m.IfName + "]/multisiteiftracking-items" } +type BFD struct { + ID string `json:"id"` + AdminSt AdminSt `json:"adminSt"` + IfkaItems struct { + DetectMult int32 `json:"detectMult"` + MinRxIntvlMs int64 `json:"minRxIntvl"` + MinTxIntvlMs int64 `json:"minTxIntvl"` + } `json:"ifka-items,omitzero"` +} + +func (*BFD) IsListItem() {} + +func (b *BFD) XPath() string { + return "System/bfd-items/inst-items/if-items/If-list[id=" + b.ID + "]" +} + +func (b *BFD) Validate() error { + if b.IfkaItems.DetectMult < 1 || b.IfkaItems.DetectMult > 50 { + return fmt.Errorf("bfd: invalid detect-mult %d: must be between 1 and 50", b.IfkaItems.DetectMult) + } + if b.IfkaItems.MinRxIntvlMs < 100 || b.IfkaItems.MinRxIntvlMs > 999 { + return fmt.Errorf("bfd: invalid min-rx-intvl %d: must be between 100 and 999", b.IfkaItems.MinRxIntvlMs) + } + if b.IfkaItems.MinTxIntvlMs < 100 || b.IfkaItems.MinTxIntvlMs > 999 { + return fmt.Errorf("bfd: invalid min-tx-intvl %d: must be between 100 and 999", b.IfkaItems.MinTxIntvlMs) + } + return nil +} + // PortChannel represents a port-channel (LAG) interface on a NX-OS device. type PortChannel struct { AccessVlan string `json:"accessVlan"` diff --git a/internal/provider/cisco/nxos/intf_test.go b/internal/provider/cisco/nxos/intf_test.go index cc361ee4..0a2eef7c 100644 --- a/internal/provider/cisco/nxos/intf_test.go +++ b/internal/provider/cisco/nxos/intf_test.go @@ -81,4 +81,10 @@ func init() { dci := &MultisiteIfTracking{IfName: "eth1/1", Tracking: MultisiteIfTrackingModeDCI} Register("bgw_tracking", dci) + + bfd := &BFD{AdminSt: AdminStEnabled, ID: "eth1/1"} + bfd.IfkaItems.DetectMult = 15 + bfd.IfkaItems.MinRxIntvlMs = 100 + bfd.IfkaItems.MinTxIntvlMs = 150 + Register("bfd", bfd) } diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index 8106ff9c..53fcfd00 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -897,6 +897,39 @@ func (p *Provider) EnsureInterface(ctx context.Context, req *provider.EnsureInte if addr != nil { conf = append(conf, addr) } + + bfd := new(BFD) + bfd.ID = name + if req.Interface.Spec.BFD != nil { + f := new(Feature) + f.Name = "bfd" + f.AdminSt = AdminStEnabled + conf = append(conf, f) + + bfd.AdminSt = AdminStDisabled + if req.Interface.Spec.BFD.Enabled { + bfd.AdminSt = AdminStEnabled + bfd.IfkaItems.MinTxIntvlMs = 50 + if req.Interface.Spec.BFD.DesiredMinimumTxInterval != nil { + bfd.IfkaItems.MinTxIntvlMs = req.Interface.Spec.BFD.DesiredMinimumTxInterval.Milliseconds() + } + bfd.IfkaItems.MinRxIntvlMs = 50 + if req.Interface.Spec.BFD.RequiredMinimumReceive != nil { + bfd.IfkaItems.MinRxIntvlMs = req.Interface.Spec.BFD.RequiredMinimumReceive.Milliseconds() + } + bfd.IfkaItems.DetectMult = 3 + if req.Interface.Spec.BFD.DetectionMultiplier != nil { + bfd.IfkaItems.DetectMult = *req.Interface.Spec.BFD.DetectionMultiplier + } + if err := bfd.Validate(); err != nil { + return err + } + } + conf = append(conf, bfd) + } else if err := p.client.Delete(ctx, bfd); err != nil { + return err + } + return p.client.Update(ctx, conf...) } diff --git a/internal/provider/cisco/nxos/testdata/bfd.json b/internal/provider/cisco/nxos/testdata/bfd.json new file mode 100644 index 00000000..18018694 --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/bfd.json @@ -0,0 +1,19 @@ +{ + "bfd-items": { + "inst-items": { + "if-items": { + "If-list": [ + { + "id": "eth1/1", + "adminSt": "enabled", + "ifka-items": { + "detectMult": 15, + "minRxIntvl": 100, + "minTxIntvl": 150 + } + } + ] + } + } + } +} diff --git a/internal/provider/cisco/nxos/testdata/bfd.json.txt b/internal/provider/cisco/nxos/testdata/bfd.json.txt new file mode 100644 index 00000000..92b7be8d --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/bfd.json.txt @@ -0,0 +1,2 @@ +interface Ethernet1/1 + bfd interval 150 min_rx 100 multiplier 15 From 998e246572eacd47334c74eed87c45d30ee0160d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20K=C3=A4stner?= Date: Wed, 10 Dec 2025 16:42:04 +0100 Subject: [PATCH 3/5] Remove bfd field from interface reference in `ISIS` resource Instead the enablement of bfd for the link is implicitly inferred from the `.spec.bfd.enabled` on the referenced `Interface`. --- api/core/v1alpha1/isis_types.go | 23 +--------- api/core/v1alpha1/zz_generated.deepcopy.go | 38 ++-------------- .../networking.metal.ironcore.dev_isis.yaml | 43 ++++++------------- .../networking.metal.ironcore.dev_isis.yaml | 43 ++++++------------- config/samples/v1alpha1_isis.yaml | 16 ++----- internal/controller/core/isis_controller.go | 15 +++---- internal/provider/cisco/nxos/provider.go | 17 +++----- internal/provider/provider.go | 7 +-- 8 files changed, 50 insertions(+), 152 deletions(-) diff --git a/api/core/v1alpha1/isis_types.go b/api/core/v1alpha1/isis_types.go index 13a9ab8e..0399f0ec 100644 --- a/api/core/v1alpha1/isis_types.go +++ b/api/core/v1alpha1/isis_types.go @@ -48,10 +48,10 @@ type ISISSpec struct { // +kubebuilder:validation:MaxItems=2 AddressFamilies []AddressFamily `json:"addressFamilies"` - // Interfaces is a list of interfaces that are part of the ISIS instance. + // InterfaceRefs is a list of interfaces that are part of the ISIS instance. // +optional // +listType=atomic - Interfaces []ISISInterface `json:"interfaces,omitempty"` + InterfaceRefs []LocalObjectReference `json:"interfaceRefs,omitempty"` } // ISISLevel represents the level of an ISIS instance. @@ -83,25 +83,6 @@ const ( AddressFamilyIPv6Unicast AddressFamily = "IPv6Unicast" ) -type ISISInterface struct { - // Ref is a reference to the interface object. - // The interface object must exist in the same namespace. - // +required - Ref LocalObjectReference `json:"ref"` - - // BFD contains BFD configuration for the interface. - // +optional - // +kubebuilder:default={} - BFD ISISBFD `json:"bfd,omitzero"` -} - -type ISISBFD struct { - // Enabled indicates whether BFD is enabled on the interface. - // +optional - // +kubebuilder:default=false - Enabled bool `json:"enabled"` -} - // ISISStatus defines the observed state of ISIS. type ISISStatus struct { // The conditions are a list of status objects that describe the state of the ISIS. diff --git a/api/core/v1alpha1/zz_generated.deepcopy.go b/api/core/v1alpha1/zz_generated.deepcopy.go index 59b1c1c5..f4fb2326 100644 --- a/api/core/v1alpha1/zz_generated.deepcopy.go +++ b/api/core/v1alpha1/zz_generated.deepcopy.go @@ -1393,38 +1393,6 @@ func (in *ISIS) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ISISBFD) DeepCopyInto(out *ISISBFD) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ISISBFD. -func (in *ISISBFD) DeepCopy() *ISISBFD { - if in == nil { - return nil - } - out := new(ISISBFD) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ISISInterface) DeepCopyInto(out *ISISInterface) { - *out = *in - out.Ref = in.Ref - out.BFD = in.BFD -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ISISInterface. -func (in *ISISInterface) DeepCopy() *ISISInterface { - if in == nil { - return nil - } - out := new(ISISInterface) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ISISList) DeepCopyInto(out *ISISList) { *out = *in @@ -1471,9 +1439,9 @@ func (in *ISISSpec) DeepCopyInto(out *ISISSpec) { *out = make([]AddressFamily, len(*in)) copy(*out, *in) } - if in.Interfaces != nil { - in, out := &in.Interfaces, &out.Interfaces - *out = make([]ISISInterface, len(*in)) + if in.InterfaceRefs != nil { + in, out := &in.InterfaceRefs, &out.InterfaceRefs + *out = make([]LocalObjectReference, len(*in)) copy(*out, *in) } } diff --git a/charts/network-operator/templates/crd/networking.metal.ironcore.dev_isis.yaml b/charts/network-operator/templates/crd/networking.metal.ironcore.dev_isis.yaml index 74419bdd..984282fd 100644 --- a/charts/network-operator/templates/crd/networking.metal.ironcore.dev_isis.yaml +++ b/charts/network-operator/templates/crd/networking.metal.ironcore.dev_isis.yaml @@ -98,40 +98,25 @@ spec: x-kubernetes-validations: - message: Instance is immutable rule: self == oldSelf - interfaces: - description: Interfaces is a list of interfaces that are part of the - ISIS instance. + interfaceRefs: + description: InterfaceRefs is a list of interfaces that are part of + the ISIS instance. items: + description: |- + LocalObjectReference contains enough information to locate a + referenced object inside the same namespace. properties: - bfd: - default: {} - description: BFD contains BFD configuration for the interface. - properties: - enabled: - default: false - description: Enabled indicates whether BFD is enabled on - the interface. - type: boolean - type: object - ref: + name: description: |- - Ref is a reference to the interface object. - The interface object must exist in the same namespace. - properties: - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - maxLength: 63 - minLength: 1 - type: string - required: - - name - type: object - x-kubernetes-map-type: atomic + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + maxLength: 63 + minLength: 1 + type: string required: - - ref + - name type: object + x-kubernetes-map-type: atomic type: array x-kubernetes-list-type: atomic networkEntityTitle: diff --git a/config/crd/bases/networking.metal.ironcore.dev_isis.yaml b/config/crd/bases/networking.metal.ironcore.dev_isis.yaml index ad9e3c56..cc7b42f3 100644 --- a/config/crd/bases/networking.metal.ironcore.dev_isis.yaml +++ b/config/crd/bases/networking.metal.ironcore.dev_isis.yaml @@ -92,40 +92,25 @@ spec: x-kubernetes-validations: - message: Instance is immutable rule: self == oldSelf - interfaces: - description: Interfaces is a list of interfaces that are part of the - ISIS instance. + interfaceRefs: + description: InterfaceRefs is a list of interfaces that are part of + the ISIS instance. items: + description: |- + LocalObjectReference contains enough information to locate a + referenced object inside the same namespace. properties: - bfd: - default: {} - description: BFD contains BFD configuration for the interface. - properties: - enabled: - default: false - description: Enabled indicates whether BFD is enabled on - the interface. - type: boolean - type: object - ref: + name: description: |- - Ref is a reference to the interface object. - The interface object must exist in the same namespace. - properties: - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - maxLength: 63 - minLength: 1 - type: string - required: - - name - type: object - x-kubernetes-map-type: atomic + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + maxLength: 63 + minLength: 1 + type: string required: - - ref + - name type: object + x-kubernetes-map-type: atomic type: array x-kubernetes-list-type: atomic networkEntityTitle: diff --git a/config/samples/v1alpha1_isis.yaml b/config/samples/v1alpha1_isis.yaml index 4cd11598..2334f3ff 100644 --- a/config/samples/v1alpha1_isis.yaml +++ b/config/samples/v1alpha1_isis.yaml @@ -17,15 +17,7 @@ spec: - IPv4Unicast - IPv6Unicast interfaces: - - ref: - name: eth1-1 - bfd: - enabled: true - - ref: - name: eth1-2 - bfd: - enabled: true - - ref: - name: lo0 - - ref: - name: lo1 + - name: eth1-1 + - name: eth1-2 + - name: lo0 + - name: lo1 diff --git a/internal/controller/core/isis_controller.go b/internal/controller/core/isis_controller.go index d49f1a35..f8006407 100644 --- a/internal/controller/core/isis_controller.go +++ b/internal/controller/core/isis_controller.go @@ -222,14 +222,14 @@ func (r *ISISReconciler) reconcile(ctx context.Context, s *isisScope) (_ ctrl.Re } } - var interfaces []provider.ISISInterface - for _, iface := range s.ISIS.Spec.Interfaces { - res := new(v1alpha1.Interface) - if err := r.Get(ctx, client.ObjectKey{Name: iface.Ref.Name, Namespace: s.ISIS.Namespace}, res); err != nil { + var interfaces []*v1alpha1.Interface + for _, iface := range s.ISIS.Spec.InterfaceRefs { + intf := new(v1alpha1.Interface) + if err := r.Get(ctx, client.ObjectKey{Name: iface.Name, Namespace: s.ISIS.Namespace}, intf); err != nil { return ctrl.Result{}, err } - if !conditions.IsReady(res) { + if !conditions.IsReady(intf) { conditions.Set(s.ISIS, metav1.Condition{ Type: v1alpha1.ReadyCondition, Status: metav1.ConditionFalse, @@ -239,10 +239,7 @@ func (r *ISISReconciler) reconcile(ctx context.Context, s *isisScope) (_ ctrl.Re return ctrl.Result{RequeueAfter: r.RequeueInterval}, nil } - interfaces = append(interfaces, provider.ISISInterface{ - Interface: res, - BFD: iface.BFD.Enabled, - }) + interfaces = append(interfaces, intf) } if err := s.Provider.Connect(ctx, s.Connection); err != nil { diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index 53fcfd00..2a9eead2 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -1078,8 +1078,8 @@ func (p *Provider) EnsureISIS(ctx context.Context, req *provider.EnsureISISReque conf := append(make([]gnmiext.Configurable, 0, 3), f) - if slices.ContainsFunc(req.Interfaces, func(intf provider.ISISInterface) bool { - return intf.BFD + if slices.ContainsFunc(req.Interfaces, func(intf *v1alpha1.Interface) bool { + return intf.Spec.BFD.Enabled }) { f := new(Feature) f.Name = "bfd" @@ -1122,12 +1122,7 @@ func (p *Provider) EnsureISIS(ctx context.Context, req *provider.EnsureISISReque dom.AfItems.DomAfList.Set(item) } - interfaces := make([]*v1alpha1.Interface, 0, len(req.Interfaces)) - for _, iface := range req.Interfaces { - interfaces = append(interfaces, iface.Interface) - } - - interfaceNames, err := p.EnsureInterfacesExist(ctx, interfaces) + interfaceNames, err := p.EnsureInterfacesExist(ctx, req.Interfaces) if err != nil { return err } @@ -1140,20 +1135,20 @@ func (p *Provider) EnsureISIS(ctx context.Context, req *provider.EnsureISISReque intf := new(ISISInterface) intf.ID = interfaceNames[i] intf.NetworkTypeP2P = AdminStOff - if iface.Interface.Spec.Type == v1alpha1.InterfaceTypePhysical { + if iface.Spec.Type == v1alpha1.InterfaceTypePhysical { intf.NetworkTypeP2P = AdminStOn } if ipv4 { intf.V4Enable = true intf.V4Bfd = "inheritVrf" - if iface.BFD { + if iface.Spec.BFD.Enabled { intf.V4Bfd = "enabled" } } if ipv6 { intf.V6Enable = true intf.V6Bfd = "inheritVrf" - if iface.BFD { + if iface.Spec.BFD.Enabled { intf.V6Bfd = "enabled" } } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 37d567de..ad7765ae 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -299,15 +299,10 @@ type ISISProvider interface { type EnsureISISRequest struct { ISIS *v1alpha1.ISIS - Interfaces []ISISInterface + Interfaces []*v1alpha1.Interface ProviderConfig *ProviderConfig } -type ISISInterface struct { - Interface *v1alpha1.Interface - BFD bool -} - type DeleteISISRequest struct { ISIS *v1alpha1.ISIS ProviderConfig *ProviderConfig From f49c3be7c0e98c624da9ad897ec48840b5d1a510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20K=C3=A4stner?= Date: Fri, 12 Dec 2025 16:11:07 +0100 Subject: [PATCH 4/5] [NX-OS] Disable icmp redirects on IPv4 interfaces running BFD sessions This patch adds support for automatically disabling icmp redirects on IPv4 links that are running BFD sessions, equivalent to the configuration of: ``` interface Ethernet1/1 no ip redirects ``` --- internal/provider/cisco/nxos/intf.go | 12 ++++++++ internal/provider/cisco/nxos/intf_test.go | 3 ++ internal/provider/cisco/nxos/provider.go | 30 +++++++++++++++++-- .../provider/cisco/nxos/testdata/rdr.json | 21 +++++++++++++ .../provider/cisco/nxos/testdata/rdr.json.txt | 2 ++ 5 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 internal/provider/cisco/nxos/testdata/rdr.json create mode 100644 internal/provider/cisco/nxos/testdata/rdr.json.txt diff --git a/internal/provider/cisco/nxos/intf.go b/internal/provider/cisco/nxos/intf.go index 67fb7aee..3a9c6055 100644 --- a/internal/provider/cisco/nxos/intf.go +++ b/internal/provider/cisco/nxos/intf.go @@ -26,6 +26,7 @@ var ( _ gnmiext.Configurable = (*SpanningTree)(nil) _ gnmiext.Configurable = (*MultisiteIfTracking)(nil) _ gnmiext.Configurable = (*BFD)(nil) + _ gnmiext.Configurable = (*ICMPIf)(nil) _ gnmiext.Configurable = (*PortChannel)(nil) _ gnmiext.Configurable = (*PortChannelOperItems)(nil) _ gnmiext.Configurable = (*SwitchVirtualInterface)(nil) @@ -199,6 +200,17 @@ func (b *BFD) Validate() error { return nil } +type ICMPIf struct { + ID string `json:"id"` + Ctrl string `json:"ctrl"` +} + +func (*ICMPIf) IsListItem() {} + +func (i *ICMPIf) XPath() string { + return "System/icmpv4-items/inst-items/dom-items/Dom-list[name=default]/if-items/If-list[id=" + i.ID + "]" +} + // PortChannel represents a port-channel (LAG) interface on a NX-OS device. type PortChannel struct { AccessVlan string `json:"accessVlan"` diff --git a/internal/provider/cisco/nxos/intf_test.go b/internal/provider/cisco/nxos/intf_test.go index 0a2eef7c..46137672 100644 --- a/internal/provider/cisco/nxos/intf_test.go +++ b/internal/provider/cisco/nxos/intf_test.go @@ -87,4 +87,7 @@ func init() { bfd.IfkaItems.MinRxIntvlMs = 100 bfd.IfkaItems.MinTxIntvlMs = 150 Register("bfd", bfd) + + icmp := &ICMPIf{ID: "eth1/1", Ctrl: "port-unreachable"} + Register("rdr", icmp) } diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index 2a9eead2..0776185d 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -906,6 +906,11 @@ func (p *Provider) EnsureInterface(ctx context.Context, req *provider.EnsureInte f.AdminSt = AdminStEnabled conf = append(conf, f) + icmp := new(ICMPIf) + icmp.ID = name + icmp.Ctrl = "port-unreachable" + conf = append(conf, icmp) + bfd.AdminSt = AdminStDisabled if req.Interface.Spec.BFD.Enabled { bfd.AdminSt = AdminStEnabled @@ -926,8 +931,21 @@ func (p *Provider) EnsureInterface(ctx context.Context, req *provider.EnsureInte } } conf = append(conf, bfd) - } else if err := p.client.Delete(ctx, bfd); err != nil { - return err + } else { + icmp := new(ICMPIf) + icmp.ID = name + switch req.Interface.Spec.Type { + case v1alpha1.InterfaceTypePhysical: + if err := p.client.Delete(ctx, icmp); err != nil { + return err + } + case v1alpha1.InterfaceTypeLoopback: + icmp.Ctrl = "port-unreachable,redirect" + conf = append(conf, icmp) + case v1alpha1.InterfaceTypeRoutedVLAN: + icmp.Ctrl = "port-unreachable" + conf = append(conf, icmp) + } } return p.client.Update(ctx, conf...) @@ -950,6 +968,10 @@ func (p *Provider) DeleteInterface(ctx context.Context, req *provider.InterfaceR } } + bfd := new(BFD) + bfd.ID = name + conf = append(conf, bfd) + switch req.Interface.Spec.Type { case v1alpha1.InterfaceTypePhysical: i := new(PhysIf) @@ -963,6 +985,10 @@ func (p *Provider) DeleteInterface(ctx context.Context, req *provider.InterfaceR conf = append(conf, stp) } + icmp := new(ICMPIf) + icmp.ID = name + conf = append(conf, icmp) + case v1alpha1.InterfaceTypeLoopback: lb := new(Loopback) lb.ID = name diff --git a/internal/provider/cisco/nxos/testdata/rdr.json b/internal/provider/cisco/nxos/testdata/rdr.json new file mode 100644 index 00000000..7bae6eca --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/rdr.json @@ -0,0 +1,21 @@ +{ + "icmpv4-items": { + "inst-items": { + "dom-items": { + "Dom-list": [ + { + "name": "default", + "if-items": { + "If-list": [ + { + "id": "eth1/1", + "ctrl": "port-unreachable" + } + ] + } + } + ] + } + } + } +} diff --git a/internal/provider/cisco/nxos/testdata/rdr.json.txt b/internal/provider/cisco/nxos/testdata/rdr.json.txt new file mode 100644 index 00000000..b38824f6 --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/rdr.json.txt @@ -0,0 +1,2 @@ +interface Ethernet1/1 + no ip redirects From 5ea9bd0d52ce44948622880f975bb0259b008c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20K=C3=A4stner?= Date: Fri, 12 Dec 2025 15:23:48 +0100 Subject: [PATCH 5/5] [NX-OS] Add ospf enablement to ospf interface reference This patch automatically infers the configuration for bfd on an ospf link according to the following config based on the `.spec.bfd.enabled` field in the `Interface` resource. ``` interface Ethernet1/1 ip ospf bfd ``` --- internal/provider/cisco/nxos/ospf.go | 11 +++++++++- internal/provider/cisco/nxos/ospf_test.go | 1 + internal/provider/cisco/nxos/provider.go | 20 +++++++++++++++++-- .../provider/cisco/nxos/testdata/ospf.json | 3 ++- 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/internal/provider/cisco/nxos/ospf.go b/internal/provider/cisco/nxos/ospf.go index fdcffc18..db4a1225 100644 --- a/internal/provider/cisco/nxos/ospf.go +++ b/internal/provider/cisco/nxos/ospf.go @@ -81,6 +81,7 @@ type OSPFInterface struct { ID string `json:"id"` NwT NtwType `json:"nwT"` PassiveCtrl PassiveControl `json:"passiveCtrl"` + BFDCtrl OspfBfdCtrl `json:"bfdCtrl"` } func (i *OSPFInterface) Key() string { return i.ID } @@ -112,7 +113,7 @@ type OSPFIfAdjEpGroup struct { OperSt AdjOperSt `json:"operSt"` // Adjacency neighbor state Prio uint8 `json:"prio"` // Priority, used in determining the designated router on this network AdjStatsItems struct { - LastStChgTs time.Time `json:"lastStChgTs"` // Timestamp of the last state change + LastStChgTS time.Time `json:"lastStChgTs"` // Timestamp of the last state change } `json:"adjstats-items,omitzero"` } @@ -202,3 +203,11 @@ const ( PassiveControlEnabled PassiveControl = "enabled" PassiveControlDisabled PassiveControl = "disabled" ) + +type OspfBfdCtrl string + +const ( + OspfBfdCtrlUnspecified OspfBfdCtrl = "unspecified" + OspfBfdCtrlEnabled OspfBfdCtrl = "enabled" + OspfBfdCtrlDisabled OspfBfdCtrl = "disabled" +) diff --git a/internal/provider/cisco/nxos/ospf_test.go b/internal/provider/cisco/nxos/ospf_test.go index e7cf4367..467bf367 100644 --- a/internal/provider/cisco/nxos/ospf_test.go +++ b/internal/provider/cisco/nxos/ospf_test.go @@ -23,6 +23,7 @@ func init() { Area: "0.0.0.0", NwT: NtwTypeUnspecified, PassiveCtrl: PassiveControlUnspecified, + BFDCtrl: OspfBfdCtrlUnspecified, } if strings.HasPrefix(name, "eth") { intf.NwT = NtwTypePointToPoint diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index 0776185d..aff42599 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -1439,13 +1439,17 @@ func (p *Provider) EnsureOSPF(ctx context.Context, req *provider.EnsureOSPFReque } } + conf := make([]gnmiext.Configurable, 0, 3) + f := new(Feature) f.Name = "ospf" f.AdminSt = AdminStEnabled + conf = append(conf, f) o := new(OSPF) o.AdminSt = AdminStEnabled o.Name = req.OSPF.Spec.Instance + conf = append(conf, o) dom := new(OSPFDom) dom.Name = DefaultVRFName @@ -1501,6 +1505,18 @@ func (p *Provider) EnsureOSPF(ctx context.Context, req *provider.EnsureOSPFReque if iface.Passive == nil || !*iface.Passive { intf.PassiveCtrl = PassiveControlDisabled } + intf.BFDCtrl = OspfBfdCtrlUnspecified + if iface.Interface.Spec.BFD != nil { + fb := new(Feature) + fb.Name = "bfd" + fb.AdminSt = AdminStEnabled + conf = slices.Insert(conf, 1, gnmiext.Configurable(fb)) // insert before OSPF + + intf.BFDCtrl = OspfBfdCtrlDisabled + if !iface.Interface.Spec.BFD.Enabled { + intf.BFDCtrl = OspfBfdCtrlEnabled + } + } dom.IfItems.IfList.Set(intf) } @@ -1528,7 +1544,7 @@ func (p *Provider) EnsureOSPF(ctx context.Context, req *provider.EnsureOSPFReque dom.MaxlsapItems.MaxLsa = cfg.MaxLSA } - return p.client.Update(ctx, f, o) + return p.client.Update(ctx, conf...) } func (p *Provider) DeleteOSPF(ctx context.Context, req *provider.DeleteOSPFRequest) error { @@ -1566,7 +1582,7 @@ func (p *Provider) GetOSPFStatus(ctx context.Context, req *provider.OSPFStatusRe Address: adj.PeerIP, Interface: i, Priority: adj.Prio, - LastEstablishedTime: adj.AdjStatsItems.LastStChgTs, + LastEstablishedTime: adj.AdjStatsItems.LastStChgTS, AdjacencyState: adj.OperSt.ToNeighborState(), }) } diff --git a/internal/provider/cisco/nxos/testdata/ospf.json b/internal/provider/cisco/nxos/testdata/ospf.json index 954a387e..ea95822e 100644 --- a/internal/provider/cisco/nxos/testdata/ospf.json +++ b/internal/provider/cisco/nxos/testdata/ospf.json @@ -23,7 +23,8 @@ "area": "0.0.0.0", "id": "eth1/1", "nwT": "p2p", - "passiveCtrl": "unspecified" + "passiveCtrl": "unspecified", + "bfdCtrl": "unspecified" } ] },