diff --git a/api/core/v1alpha1/interface_types.go b/api/core/v1alpha1/interface_types.go index 987a7c0b..ba29e679 100644 --- a/api/core/v1alpha1/interface_types.go +++ b/api/core/v1alpha1/interface_types.go @@ -141,6 +141,12 @@ type Switchport struct { // +kubebuilder:validation:Maximum=4094 AccessVlan int32 `json:"accessVlan,omitempty"` + // InnerVlan specifies the VLAN id for QinQ access mode switchports. + // +optional + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=4094 + InnerVlan int32 `json:"innerVlan,omitempty"` + // NativeVlan specifies the native VLAN ID for trunk mode switchports. // Only applicable when Mode is set to "Trunk". // +optional diff --git a/config/crd/bases/networking.metal.ironcore.dev_interfaces.yaml b/config/crd/bases/networking.metal.ironcore.dev_interfaces.yaml index ec6df376..a34a36c0 100644 --- a/config/crd/bases/networking.metal.ironcore.dev_interfaces.yaml +++ b/config/crd/bases/networking.metal.ironcore.dev_interfaces.yaml @@ -336,6 +336,13 @@ spec: type: integer minItems: 1 type: array + innerVlan: + description: InnerVlan specifies the VLAN id for QinQ access mode + switchports. + format: int32 + maximum: 4094 + minimum: 1 + type: integer mode: description: Mode defines the switchport mode, such as access or trunk. diff --git a/internal/provider/cisco/gnmiext/v2/client.go b/internal/provider/cisco/gnmiext/v2/client.go index bb20d5df..3ace1db3 100644 --- a/internal/provider/cisco/gnmiext/v2/client.go +++ b/internal/provider/cisco/gnmiext/v2/client.go @@ -18,6 +18,8 @@ import ( "github.com/openconfig/ygot/ygot" "github.com/tidwall/gjson" "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) // Configurable represents a configuration item with a YANG path. @@ -272,16 +274,22 @@ func (c *client) set(ctx context.Context, patch bool, conf ...Configurable) erro return err } got := cp.Deep(cf) + err = c.GetConfig(ctx, got) - if err != nil && !errors.Is(err, ErrNil) { - return fmt.Errorf("gnmiext: failed to retrieve current config for %s: %w", cf.XPath(), err) - } - // If the current configuration is equal to the desired configuration, skip the update. - // This avoids unnecessary updates and potential disruptions. - if err == nil && reflect.DeepEqual(cf, got) { - c.logger.V(1).Info("Configuration is already up-to-date", "path", cf.XPath()) - continue + + // If the current configuration does not exist, continue to set the desired configuration. + if status.Code(err) != codes.NotFound { + if err != nil && !errors.Is(err, ErrNil) { + return fmt.Errorf("gnmiext: failed to retrieve current config for %s: %w", cf.XPath(), err) + } + // If the current configuration is equal to the desired configuration, skip the update. + // This avoids unnecessary updates and potential disruptions. + if err == nil && reflect.DeepEqual(cf, got) { + c.logger.V(1).Info("Configuration is already up-to-date", "path", cf.XPath()) + continue + } } + b, err := c.Marshal(cf) if err != nil { return err diff --git a/internal/provider/cisco/gnmiext/v2/empty.go b/internal/provider/cisco/gnmiext/v2/empty.go index 1bcca084..ef4e8c2a 100644 --- a/internal/provider/cisco/gnmiext/v2/empty.go +++ b/internal/provider/cisco/gnmiext/v2/empty.go @@ -6,6 +6,7 @@ package gnmiext import ( "encoding/json" "fmt" + "regexp" ) // NOTE: Use json.Marshaler and json.Unmarshaler interfaces instead of the @@ -39,7 +40,10 @@ func (e *Empty) UnmarshalJSON(b []byte) error { *e = false return nil } - if string(b) != "[null]" { + + // Due to some Cisco IOSX ouptut we also match [ \n null \n] + nullTypeRe := regexp.MustCompile(`^\[\s*null\s*]$`) + if !nullTypeRe.MatchString(string(b)) { return fmt.Errorf("gnmiext: invalid empty value: %s", string(b)) } *e = true diff --git a/internal/provider/cisco/iosxr/intf.go b/internal/provider/cisco/iosxr/intf.go index 76045b68..b002e35c 100644 --- a/internal/provider/cisco/iosxr/intf.go +++ b/internal/provider/cisco/iosxr/intf.go @@ -4,23 +4,72 @@ package iosxr import ( + "errors" "fmt" "regexp" + "strconv" + "strings" "github.com/ironcore-dev/network-operator/internal/provider/cisco/gnmiext/v2" ) -type PhysIf struct { +type IFaceSpeed string + +const ( + Speed10G IFaceSpeed = "TenGigE" + Speed25G IFaceSpeed = "TwentyFiveGigE" + Speed40G IFaceSpeed = "FortyGigE" + Speed100G IFaceSpeed = "HundredGigE" + EtherBundle IFaceSpeed = "etherbundle" +) + +type BunldePortActivity string + +const ( + PortActivityOn BunldePortActivity = "on" + PortActivityActive BunldePortActivity = "active" + PortActivityPassive BunldePortActivity = "passive" + PortActivityInherit BunldePortActivity = "inherit" +) + +type PhysIfStateType string + +const ( + StateUp PhysIfStateType = "im-state-up" + StateDown PhysIfStateType = "im-state-down" + StateNotReady PhysIfStateType = "im-state-not-ready" + StateAdminDown PhysIfStateType = "im-state-admin-down" + StateShutDown PhysIfStateType = "im-state-shutdown" +) + +// Represent physical and bundle interfaces as part of the same struct as they share a lot of common configuration +// and only differ in a few attributes like the interface name and the presence of bundle configuration or not. +type Iface struct { Name string `json:"-"` - Description string `json:"description"` - Active string `json:"active"` - Vrf string `json:"Cisco-IOS-XR-infra-rsi-cfg:vrf,omitempty"` - Statistics Statistics `json:"Cisco-IOS-XR-infra-statsd-cfg:statistics,omitempty"` - IPv4Network IPv4Network `json:"Cisco-IOS-XR-ipv4-io-cfg:ipv4-network,omitempty"` - IPv6Network IPv6Network `json:"Cisco-IOS-XR-ipv6-ma-cfg:ipv6-network,omitempty"` - IPv6Neighbor IPv6Neighbor `json:"Cisco-IOS-XR-ipv6-nd-cfg:ipv6-neighbor,omitempty"` - MTUs MTUs `json:"mtus,omitempty"` - Shutdown gnmiext.Empty `json:"shutdown,omitempty"` + Description string `json:"description,omitzero"` + Statistics Statistics `json:"Cisco-IOS-XR-infra-statsd-cfg:statistics,omitzero"` + MTUs MTUs `json:"mtus,omitzero"` + Active string `json:"active,omitzero"` + Vrf string `json:"Cisco-IOS-XR-infra-rsi-cfg:vrf,omitzero"` + IPv4Network IPv4Network `json:"Cisco-IOS-XR-ipv4-io-cfg:ipv4-network,omitzero"` + IPv6Network IPv6Network `json:"Cisco-IOS-XR-ipv6-ma-cfg:ipv6-network,omitzero"` + IPv6Neighbor IPv6Neighbor `json:"Cisco-IOS-XR-ipv6-nd-cfg:ipv6-neighbor,omitzero"` + Shutdown gnmiext.Empty `json:"shutdown,omitzero"` + + // Existence of this object causes the creation of the software subinterface + ModeNoPhysical string `json:"interface-mode-non-physical,omitzero"` + + // BundleMember configuration for Physical interface as member of a Bundle-Ether + BundleMember BundleMember `json:"Cisco-IOS-XR-bundlemgr-cfg:bundle-member,omitzero"` + + // Mode in which an interface is running (e.g., virtual for subinterfaces) + Mode gnmiext.Empty `json:"interface-virtual,omitzero"` + Bundle Bundle `json:"Cisco-IOS-XR-bundlemgr-cfg:bundle,omitzero"` + SubInterface VlanSubInterface `json:"Cisco-IOS-XR-l2-eth-infra-cfg:vlan-sub-configuration,omitzero"` +} + +type BundleMember struct { + ID BundleID `json:"id"` } type Statistics struct { @@ -29,7 +78,7 @@ type Statistics struct { type IPv4Network struct { Addresses AddressesIPv4 `json:"addresses"` - Mtu uint16 `json:"mtu"` + Mtu uint16 `json:"mtu,omitzero"` } type AddressesIPv4 struct { @@ -73,64 +122,157 @@ type MTU struct { Owner string `json:"owner"` } -func (i *PhysIf) XPath() string { +type BundleID struct { + BundleID int32 `json:"bundle-id"` + PortAcivity string `json:"port-activity"` +} + +type Bundle struct { + MinAct MinimumActive `json:"minimum-active"` +} + +type MinimumActive struct { + Links int32 `json:"links"` +} + +type VlanSubInterface struct { + VlanIdentifier VlanIdentifier `json:"vlan-identifier"` +} + +type VlanIdentifier struct { + FirstTag int32 `json:"first-tag"` + SecondTag int32 `json:"second-tag,omitzero"` + VlanType string `json:"vlan-type"` +} + +func (i *Iface) XPath() string { return fmt.Sprintf("Cisco-IOS-XR-ifmgr-cfg:interface-configurations/interface-configuration[active=act][interface-name=%s]", i.Name) } -func (i *PhysIf) String() string { - return fmt.Sprintf("Name: %s, Description=%s, ShutDown=%t", i.Name, i.Description, i.Shutdown) +func (i *Iface) String() string { + return fmt.Sprintf("Name: %s, Description=%s", i.Name, i.Description) } -type IFaceSpeed string +type PhysIfState struct { + State string `json:"state"` + Name string `json:"-"` +} -const ( - Speed10G IFaceSpeed = "TenGigE" - Speed25G IFaceSpeed = "TwentyFiveGigE" - Speed40G IFaceSpeed = "FortyGigE" - Speed100G IFaceSpeed = "HundredGigE" -) +func (phys *PhysIfState) XPath() string { + // (fixme): hardcoded route processor for the moment + return fmt.Sprintf("Cisco-IOS-XR-ifmgr-oper:interface-properties/data-nodes/data-node[data-node-name=0/RP0/CPU0]/system-view/interfaces/interface[interface-name=%s]", phys.Name) +} + +func ExractInterfaceSpeedFromName(ifaceName string) (IFaceSpeed, error) { + // Owner of bundle interfaces is 'etherbundle' + bundleEtherRE := regexp.MustCompile(`^Bundle-Ether*`) + if bundleEtherRE.MatchString(ifaceName) { + // For Bundle-Ether interfaces + return EtherBundle, nil + } -func ExtractMTUOwnerFromIfaceName(ifaceName string) (IFaceSpeed, error) { // Match the port_type in an interface name /// // E.g. match TwentyFiveGigE of interface with name TwentyFiveGigE0/0/0/1 re := regexp.MustCompile(`^\D*`) - - mtuOwner := string(re.Find([]byte(ifaceName))) - - if mtuOwner == "" { - return "", fmt.Errorf("failed to extract MTU owner from interface name %s", ifaceName) + speed := string(re.Find([]byte(ifaceName))) + if speed == "" { + return "", fmt.Errorf("failed to extract speed from interface name %s", ifaceName) } - switch mtuOwner { + switch speed { case string(Speed10G): return Speed10G, nil case string(Speed25G): return Speed25G, nil case string(Speed40G): - return Speed25G, nil + return Speed40G, nil case string(Speed100G): return Speed100G, nil default: - return "", fmt.Errorf("unsupported interface type %s for MTU owner extraction", mtuOwner) + return "", fmt.Errorf("unsupported interface type %s§§", speed) } } -type PhysIfStateType string +func CheckInterfaceNameTypeAggregate(name string) error { + if name == "" { + return errors.New("interface name must not be empty") + } + // Matches Bundle-Ether[.] or BE[.] + re := regexp.MustCompile(`^(Bundle-Ether|BE)(\d+)(\.(\d+))?$`) + matches := re.FindStringSubmatch(name) -const ( - StateUp PhysIfStateType = "im-state-up" - StateDown PhysIfStateType = "im-state-down" - StateNotReady PhysIfStateType = "im-state-not-ready" - StateAdminDown PhysIfStateType = "im-state-admin-down" - StateShutDown PhysIfStateType = "im-state-shutdown" -) + if matches == nil { + return fmt.Errorf("unsupported interface format %q, expected one of: %q", name, re.String()) + } -type PhysIfState struct { - State string `json:"state"` - Name string `json:"-"` + // Vlan is part of the name + if matches[2] == "" { + return fmt.Errorf("unsupported interface format %q, expected one of: %q", name, re.String()) + } + // Check outer vlan + // fixme: check range up to 65000 + // err := CheckVlanRange(matches[2]) + + // Check inner vlan if we have a subinterface + if matches[4] != "" { + return CheckVlanRange(matches[4]) + } + return nil } -func (phys *PhysIfState) XPath() string { - // (fixme): hardcoded route processor for the moment - return fmt.Sprintf("Cisco-IOS-XR-ifmgr-oper:interface-properties/data-nodes/data-node[data-node-name=0/RP0/CPU0]/system-view/interfaces/interface[interface-name=%s]", phys.Name) +func ExtractVlanTagFromName(name string) (vlanID int32, err error) { + // TF0/0/0/3.2021 or Te0/0/0/3.2021 or Hun0/0/0/3.2021 + res := strings.Split(name, ".") + switch len(res) { + case 1: + return 0, nil + case 2: + vlan, err := strconv.ParseInt(res[1], 10, 32) + if err != nil { + return 0, fmt.Errorf("failed to parse VLAN ID from interface name %q: %w", name, err) + } + return int32(vlan), nil + default: + return 0, fmt.Errorf("unexpected interface name format %q, expected or .", name) + } +} + +func ExtractBundleIdAndVlanTagsFromName(name string) (bundleID, outerVlan int32, err error) { + // Matches BE1.1 or Bundle-Ether1.1 + re := regexp.MustCompile(`^(Bundle-Ether|BE)(\d+)(?:\.(\d+))?$`) + matches := re.FindStringSubmatch(name) + + switch len(matches) { + case 3: + o, err := strconv.ParseInt(matches[2], 10, 32) + if err != nil { + return 0, 0, fmt.Errorf("failed to parse bundle ID from interface name %q: %w", name, err) + } + bundleID = int32(o) + case 4: + o, err := strconv.ParseInt(matches[2], 10, 32) + if err != nil { + return 0, 0, fmt.Errorf("failed to parse bundle ID from interface name %q: %w", name, err) + } + i, err := strconv.ParseInt(matches[3], 10, 32) + if err != nil { + return 0, 0, fmt.Errorf("failed to parse outer VLAN from interface name %q: %w", name, err) + } + bundleID = int32(o) + outerVlan = int32(i) + } + return bundleID, outerVlan, nil +} + +func CheckVlanRange(vlan string) error { + v, err := strconv.Atoi(vlan) + + if err != nil { + return fmt.Errorf("failed to parse VLAN %q: %w", vlan, err) + } + + if v < 1 || v > 4095 { + return fmt.Errorf("VLAN %s is out of range, valid range is 1-4095", vlan) + } + return nil } diff --git a/internal/provider/cisco/iosxr/intf_test.go b/internal/provider/cisco/iosxr/intf_test.go index c5cc84d3..03cfba01 100644 --- a/internal/provider/cisco/iosxr/intf_test.go +++ b/internal/provider/cisco/iosxr/intf_test.go @@ -11,7 +11,7 @@ func init() { Owner: "TwentyFiveGigE", } - Register("intf", &PhysIf{ + Register("intf", &Iface{ Name: name, Description: "random interface test", Active: "act", diff --git a/internal/provider/cisco/iosxr/provider.go b/internal/provider/cisco/iosxr/provider.go index 7bc3c4b2..6cd7dd4a 100644 --- a/internal/provider/cisco/iosxr/provider.go +++ b/internal/provider/cisco/iosxr/provider.go @@ -7,15 +7,16 @@ import ( "context" "errors" "fmt" - "strconv" + "strings" + cp "github.com/felix-kaestner/copy" + "google.golang.org/grpc" + + "github.com/ironcore-dev/network-operator/api/core/v1alpha1" "github.com/ironcore-dev/network-operator/internal/deviceutil" "github.com/ironcore-dev/network-operator/internal/provider" "github.com/ironcore-dev/network-operator/internal/provider/cisco/gnmiext/v2" - - "github.com/ironcore-dev/network-operator/api/core/v1alpha1" - - "google.golang.org/grpc" + "github.com/ironcore-dev/network-operator/internal/util" ) var ( @@ -53,82 +54,211 @@ func (p *Provider) EnsureInterface(ctx context.Context, req *provider.EnsureInte return errors.New("client is not connected") } - if req.Interface.Spec.Type != v1alpha1.InterfaceTypePhysical { - message := "unsupported interface type for interface " + req.Interface.Spec.Name - return errors.New(message) + name := req.Interface.Spec.Name + + // Configure different inteface types based on the interface name + // Interface e.g TwentyFiveGigE0/0/0/3 + // SubInterface . e.g TwentyFiveGigE0/0/0/3 + // Bundle Interface/Port Channel Bundle-Ether + // Vlans over Bundle Bundle-Ether. + _, err := ExractInterfaceSpeedFromName(name) + if err != nil { + return err } - name := req.Interface.Spec.Name + conf := make([]gnmiext.Configurable, 0, 2) + + switch req.Interface.Spec.Type { + case v1alpha1.InterfaceTypePhysical: + + iface := &Iface{} + iface.Name = name + iface.Description = req.Interface.Spec.Description + + // Check if interface is part of a bundle + // Bundle configuration needs to happen in a sperate gnmi call + bundle_name := req.Interface.GetAnnotations()[v1alpha1.AggregateLabel] + if bundle_name == "" { + iface.Statistics.LoadInterval = uint8(30) + + vlan, err := ExtractVlanTagFromName(name) + if err != nil { + return err + } + + // Configure Subinterface + if vlan != 0 { + iface.SubInterface = NewVlanSubinterface(vlan, 0, "vlan-type-dot1q") + iface.ModeNoPhysical = "default" + } + + if req.Interface.Spec.IPv4 != nil { + if len(req.Interface.Spec.IPv4.Addresses) > 1 { + message := "multiple IPv4 addresses configured for interface " + name + return errors.New(message) + } + + // (fixme): support IPv6 addresses, IPv6 neighbor config + prefix := req.Interface.Spec.IPv4.Addresses[0] + ip := util.GetIPAddressFromIPPrefix(prefix) + netmask := util.GetCIDRFromIPPrefix(prefix) + + iface.IPv4Network = IPv4Network{ + Addresses: AddressesIPv4{ + Primary: Primary{ + Address: ip, + Netmask: netmask, + }, + }, + } + } + + if req.Interface.Spec.MTU != 0 { + mtu, err := NewMTU(name, req.Interface.Spec.MTU) + if err != nil { + return err + } + iface.MTUs = mtu + } + } - physif := &PhysIf{} + // Make interface part of a bundle + if bundle_name != "" { + ifaceBundeConf := &Iface{} + ifaceBundeConf.Name = name + bundle_id, _, err := ExtractBundleIdAndVlanTagsFromName(bundle_name) + if err != nil { + return err + } + ifaceBundeConf.BundleMember = BundleMember{ + ID: BundleID{ + BundleID: bundle_id, + PortAcivity: string(PortActivityOn), + }, + } + iface = ifaceBundeConf + } - physif.Name = req.Interface.Spec.Name - physif.Description = req.Interface.Spec.Description + // (fixme): for the moment it is enought to keep this static + // option1: extend existing interface spec + // option2: create a custom iosxr config + iface.Shutdown = gnmiext.Empty(false) + if req.Interface.Spec.AdminState == v1alpha1.AdminStateDown { + iface.Shutdown = gnmiext.Empty(true) + } + conf = append(conf, iface) - physif.Statistics.LoadInterval = 30 - owner, err := ExtractMTUOwnerFromIfaceName(name) - if err != nil { - message := "failed to extract MTU owner from interface name" + name - return errors.New(message) - } - physif.MTUs = MTUs{MTU: []MTU{{MTU: req.Interface.Spec.MTU, Owner: string(owner)}}} - - // (fixme): for the moment it is enough to keep this static - // option1: extend existing interface spec - // option2: create a custom iosxr config - physif.Shutdown = gnmiext.Empty(false) - if req.Interface.Spec.AdminState == v1alpha1.AdminStateDown { - physif.Shutdown = gnmiext.Empty(true) - } - physif.Statistics.LoadInterval = uint8(30) + return updateInterface(ctx, p.client, conf...) + case v1alpha1.InterfaceTypeAggregate: + if err := CheckInterfaceNameTypeAggregate(name); err != nil { + return err + } - if len(req.Interface.Spec.IPv4.Addresses) == 0 { - message := "no IPv4 address configured for interface " + name - return errors.New(message) - } + iface := NewBundleInterface(req.Interface) - if len(req.Interface.Spec.IPv4.Addresses) > 1 { - message := "multiple IPv4 addresses configured for interface " + name - return errors.New(message) + // Presence of an outerVlan Tag in the interface name indicates a subinterface + // BE. + _, outerVlan, err := ExtractBundleIdAndVlanTagsFromName(name) + if err != nil { + return err + } + + if outerVlan != 0 { + if req.Interface.Spec.Switchport != nil && outerVlan != req.Interface.Spec.Switchport.AccessVlan { + message := fmt.Sprintf("AccesVlan must match bundle-ether name pattern BE.. %d != %d", + outerVlan, req.Interface.Spec.Switchport.AccessVlan) + return errors.New(message) + } + + // Unset for bundle subinterfaces + iface.Mode = gnmiext.Empty(false) + + // make sure the parent bundle-ether interface bundle-ether exits + parentBunndle := strings.Split(name, ".")[0] + tmp := cp.Deep(req.Interface) + tmp.Spec.Name = parentBunndle + bundle := NewBundleInterface(tmp) + conf = append(conf, &bundle) + + iface.ModeNoPhysical = "default" + iface.SubInterface = VlanSubInterface{ + VlanIdentifier: VlanIdentifier{ + FirstTag: outerVlan, + VlanType: "vlan-type-dot1q", + }, + } + + // Subinterface configures QAndQ vlan + if req.Interface.Spec.Switchport != nil && req.Interface.Spec.Switchport.AccessVlan != 0 { + iface.SubInterface.VlanIdentifier.SecondTag = req.Interface.Spec.Switchport.AccessVlan + iface.SubInterface.VlanIdentifier.VlanType = "vlan-type-dot1ad" + } + conf = append(conf, &iface) + } else { + iface.Statistics.LoadInterval = uint8(30) + + mtu, err := NewMTU(name, req.Interface.Spec.MTU) + if err != nil { + return err + } + iface.MTUs = mtu + + iface.Bundle = Bundle{ + MinAct: MinimumActive{ + Links: 1, + }, + } + conf = append(conf, &iface) + } + return updateInterface(ctx, p.client, conf...) } + return nil +} - // (fixme): support IPv6 addresses, IPv6 neighbor config - ip := req.Interface.Spec.IPv4.Addresses[0].Addr().String() - ipNet := req.Interface.Spec.IPv4.Addresses[0].Bits() - - physif.IPv4Network = IPv4Network{ - Addresses: AddressesIPv4{ - Primary: Primary{ - Address: ip, - Netmask: strconv.Itoa(ipNet), - }, - }, +func NewBundleInterface(req *v1alpha1.Interface) Iface { + bundle := Iface{ + Name: req.Spec.Name, + Description: req.Spec.Description, + // Set Interface mode to virtual for bundle interfaces + Mode: gnmiext.Empty(true), } + return bundle +} + +func NewVlanSubinterface(firstTag, secondTag int32, vlanType string) VlanSubInterface { + subInt := VlanSubInterface{} - // Check if interface exists otherwise patch will fail - tmpPhysif := &PhysIf{} - tmpPhysif.Name = name + subInt.VlanIdentifier.FirstTag = firstTag + subInt.VlanIdentifier.SecondTag = secondTag + subInt.VlanIdentifier.VlanType = vlanType + return subInt +} - err = p.client.GetConfig(ctx, tmpPhysif) +func NewMTU(intName string, mtu int32) (MTUs, error) { + owner, err := ExractInterfaceSpeedFromName(intName) if err != nil { - // Interface does not exist, create it - err = p.client.Update(ctx, physif) - if err != nil { - return fmt.Errorf("failed to create interface %s: %w", req.Interface.Spec.Name, err) - } - return nil + message := "failed to extract MTU owner from interface name" + intName + return MTUs{}, errors.New(message) } + return MTUs{MTU: []MTU{{ + MTU: mtu, + Owner: string(owner), + }}}, nil +} - err = p.client.Update(ctx, physif) - if err != nil { +func updateInterface(ctx context.Context, client gnmiext.Client, conf ...gnmiext.Configurable) error { + for _, cf := range conf { + err := client.Update(ctx, cf) + if err == nil { + continue + } return err } - return nil } func (p *Provider) DeleteInterface(ctx context.Context, req *provider.InterfaceRequest) error { - physif := &PhysIf{} + physif := &Iface{} physif.Name = req.Interface.Spec.Name if p.client == nil { diff --git a/internal/provider/cisco/iosxr/provider_test.go b/internal/provider/cisco/iosxr/provider_test.go index 55e90be3..74214876 100644 --- a/internal/provider/cisco/iosxr/provider_test.go +++ b/internal/provider/cisco/iosxr/provider_test.go @@ -116,6 +116,10 @@ func (m *MockClient) GetConfig(ctx context.Context, conf ...gnmiext.Configurable return nil } +func (m *MockClient) Create(ctx context.Context, conf ...gnmiext.Configurable) error { + return nil +} + func (m *MockClient) GetState(ctx context.Context, conf ...gnmiext.Configurable) error { if m.GetStateFunc != nil { return m.GetStateFunc(ctx, conf...) diff --git a/internal/util/ip.go b/internal/util/ip.go new file mode 100644 index 00000000..5f3e6a9d --- /dev/null +++ b/internal/util/ip.go @@ -0,0 +1,17 @@ +package util + +import ( + "net" + + "github.com/ironcore-dev/network-operator/api/core/v1alpha1" +) + +func GetCIDRFromIPPrefix(ip v1alpha1.IPPrefix) string { + bits := ip.Bits() + mask := net.CIDRMask(bits, 32) + return net.IP(mask).String() +} + +func GetIPAddressFromIPPrefix(ip v1alpha1.IPPrefix) string { + return ip.Addr().String() +}