diff --git a/api/core/v1alpha1/interface_types.go b/api/core/v1alpha1/interface_types.go index ea81958d..891d2e27 100644 --- a/api/core/v1alpha1/interface_types.go +++ b/api/core/v1alpha1/interface_types.go @@ -18,6 +18,7 @@ import ( // +kubebuilder:validation:XValidation:rule="self.type == 'RoutedVLAN' || !has(self.vlanRef)", message="vlanRef must only be specified on interfaces of type RoutedVLAN" // +kubebuilder:validation:XValidation:rule="self.type != 'RoutedVLAN' || !has(self.switchport)", message="switchport must not be specified for interfaces of type RoutedVLAN" // +kubebuilder:validation:XValidation:rule="self.type != 'RoutedVLAN' || !has(self.aggregation)", message="aggregation must not be specified for interfaces of type RoutedVLAN" +// +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" type InterfaceSpec struct { @@ -158,6 +159,7 @@ const ( // InterfaceIPv4 defines the IPv4 configuration for an interface. // +kubebuilder:validation:XValidation:rule="!has(self.addresses) || !has(self.unnumbered)", message="addresses and unnumbered are mutually exclusive" +// +kubebuilder:validation:XValidation:rule="!has(self.unnumbered) || !self.anycastGateway", message="anycastGateway and unnumbered are mutually exclusive" type InterfaceIPv4 struct { // Addresses defines the list of IPv4 addresses assigned to the interface. // The first address in the list is considered the primary address, @@ -171,6 +173,14 @@ type InterfaceIPv4 struct { // When specified, the interface borrows the IP address from another interface. // +optional Unnumbered *InterfaceIPv4Unnumbered `json:"unnumbered,omitempty"` + + // AnycastGateway enables distributed anycast gateway functionality. + // When enabled, this interface uses the virtual MAC configured in the + // device's NVE resource for active-active default gateway redundancy. + // Only applicable for RoutedVLAN interfaces in EVPN/VXLAN fabrics. + // +optional + // +kubebuilder:default=false + AnycastGateway bool `json:"anycastGateway,omitempty"` } // InterfaceIPv4Unnumbered defines the unnumbered interface configuration. 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 a5cc2151..ae2a359d 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 @@ -191,6 +191,14 @@ spec: minItems: 1 type: array x-kubernetes-list-type: atomic + anycastGateway: + default: false + description: |- + AnycastGateway enables distributed anycast gateway functionality. + When enabled, this interface uses the virtual MAC configured in the + device's NVE resource for active-active default gateway redundancy. + Only applicable for RoutedVLAN interfaces in EVPN/VXLAN fabrics. + type: boolean unnumbered: description: |- Unnumbered defines the unnumbered interface configuration. @@ -219,6 +227,8 @@ spec: x-kubernetes-validations: - message: addresses and unnumbered are mutually exclusive rule: '!has(self.addresses) || !has(self.unnumbered)' + - message: anycastGateway and unnumbered are mutually exclusive + rule: '!has(self.unnumbered) || !self.anycastGateway' mtu: description: MTU (Maximum Transmission Unit) specifies the size of the largest packet that can be sent over the interface. @@ -391,6 +401,8 @@ spec: rule: self.type != 'RoutedVLAN' || !has(self.switchport) - message: aggregation must not be specified for interfaces of type RoutedVLAN rule: self.type != 'RoutedVLAN' || !has(self.aggregation) + - message: anycastGateway can only be enabled for interfaces of type RoutedVLAN + rule: self.type == 'RoutedVLAN' || !has(self.ipv4) || !self.ipv4.anycastGateway - message: vrfRef must not be specified for interfaces of type Aggregate rule: self.type != 'Aggregate' || !has(self.vrfRef) - message: vrfRef must not be specified for Physical interfaces with switchport diff --git a/config/crd/bases/networking.metal.ironcore.dev_interfaces.yaml b/config/crd/bases/networking.metal.ironcore.dev_interfaces.yaml index 1b2f3958..95e124c8 100644 --- a/config/crd/bases/networking.metal.ironcore.dev_interfaces.yaml +++ b/config/crd/bases/networking.metal.ironcore.dev_interfaces.yaml @@ -185,6 +185,14 @@ spec: minItems: 1 type: array x-kubernetes-list-type: atomic + anycastGateway: + default: false + description: |- + AnycastGateway enables distributed anycast gateway functionality. + When enabled, this interface uses the virtual MAC configured in the + device's NVE resource for active-active default gateway redundancy. + Only applicable for RoutedVLAN interfaces in EVPN/VXLAN fabrics. + type: boolean unnumbered: description: |- Unnumbered defines the unnumbered interface configuration. @@ -213,6 +221,8 @@ spec: x-kubernetes-validations: - message: addresses and unnumbered are mutually exclusive rule: '!has(self.addresses) || !has(self.unnumbered)' + - message: anycastGateway and unnumbered are mutually exclusive + rule: '!has(self.unnumbered) || !self.anycastGateway' mtu: description: MTU (Maximum Transmission Unit) specifies the size of the largest packet that can be sent over the interface. @@ -385,6 +395,8 @@ spec: rule: self.type != 'RoutedVLAN' || !has(self.switchport) - message: aggregation must not be specified for interfaces of type RoutedVLAN rule: self.type != 'RoutedVLAN' || !has(self.aggregation) + - message: anycastGateway can only be enabled for interfaces of type RoutedVLAN + rule: self.type == 'RoutedVLAN' || !has(self.ipv4) || !self.ipv4.anycastGateway - message: vrfRef must not be specified for interfaces of type Aggregate rule: self.type != 'Aggregate' || !has(self.vrfRef) - message: vrfRef must not be specified for Physical interfaces with switchport diff --git a/config/samples/v1alpha1_interface.yaml b/config/samples/v1alpha1_interface.yaml index 633ba14c..6c34b528 100644 --- a/config/samples/v1alpha1_interface.yaml +++ b/config/samples/v1alpha1_interface.yaml @@ -155,6 +155,7 @@ spec: ipv4: addresses: - 192.168.10.254/24 + anycastGateway: true --- apiVersion: networking.metal.ironcore.dev/v1alpha1 kind: Interface diff --git a/internal/controller/core/interface_controller_test.go b/internal/controller/core/interface_controller_test.go index 2acb9318..6a64d06d 100644 --- a/internal/controller/core/interface_controller_test.go +++ b/internal/controller/core/interface_controller_test.go @@ -63,6 +63,13 @@ var _ = Describe("Interface Controller", func() { By("Cleaning up the test Device resource") Expect(k8sClient.Delete(ctx, device, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) + By("Verifying all Interfaces are deleted") + Eventually(func(g Gomega) { + intfList := &v1alpha1.InterfaceList{} + g.Expect(k8sClient.List(ctx, intfList, client.InNamespace(metav1.NamespaceDefault))).To(Succeed()) + g.Expect(intfList.Items).To(BeEmpty()) + }).Should(Succeed()) + By("Verifying the Interface is removed from the provider") Eventually(func(g Gomega) { g.Expect(testProvider.Ports.Has(name)).To(BeFalse(), "Provider shouldn't have Interface configured anymore") diff --git a/internal/provider/cisco/nxos/intf_test.go b/internal/provider/cisco/nxos/intf_test.go index 1c4b91e4..956f7004 100644 --- a/internal/provider/cisco/nxos/intf_test.go +++ b/internal/provider/cisco/nxos/intf_test.go @@ -71,4 +71,11 @@ func init() { VlanID: 10, } Register("svi", svi) + + fwif := &FabricFwdIf{ + AdminSt: AdminStEnabled, + ID: "vlan10", + Mode: FwdModeAnycastGateway, + } + Register("fwif", fwif) } diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index d2822575..54114464 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -773,6 +773,20 @@ func (p *Provider) EnsureInterface(ctx context.Context, req *provider.EnsureInte svi.VlanID = req.VLAN.Spec.ID svi.RtvrfMbrItems = NewVrfMember(name, vrf) + fwif := new(FabricFwdIf) + fwif.ID = name + + switch { + case req.Interface.Spec.IPv4 != nil && req.Interface.Spec.IPv4.AnycastGateway: + fwif.AdminSt = AdminStEnabled + fwif.Mode = FwdModeAnycastGateway + conf = append(conf, fwif) + default: + if err := p.client.Delete(ctx, fwif); err != nil { + return err + } + } + conf = append(conf, svi) default: diff --git a/internal/provider/cisco/nxos/testdata/fwif.json b/internal/provider/cisco/nxos/testdata/fwif.json new file mode 100644 index 00000000..76469f1a --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/fwif.json @@ -0,0 +1,15 @@ +{ + "hmm-items": { + "fwdinst-items": { + "if-items": { + "FwdIf-list": [ + { + "adminSt": "enabled", + "id": "vlan10", + "mode": "anycastGW" + } + ] + } + } + } +} diff --git a/internal/provider/cisco/nxos/testdata/fwif.json.txt b/internal/provider/cisco/nxos/testdata/fwif.json.txt new file mode 100644 index 00000000..fa0af9c8 --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/fwif.json.txt @@ -0,0 +1,2 @@ +interface Vlan10 + fabric forwarding mode anycast-gateway