Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 32 additions & 41 deletions internal/provider/cisco/iosxr/intf.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
)

var (
bundleEtherRE = regexp.MustCompile(`^Bundle-Ether(\d+)(?:\.\d+)?$`)
bundleEtherRE = regexp.MustCompile(`^(Bundle-Ether|bundle-ether)(\d+)(?:\.\d+)?$`)
physicalInterfaceRE = regexp.MustCompile(`^(TenGigE|TwentyFiveGigE|FortyGigE|HundredGigE|GigabitEthernet)(\d){1}(\/\d){2}(\/\d+){1}(.\d{1,5})?$`)
loopbackInterfaceRE = regexp.MustCompile(`^(Loopback|Lo)\d+$`)
mgmtEthInterfaceRe = regexp.MustCompile(`^MgmtEth\d+\/RP\d+\/CPU\d+\/\d+$`)
Expand Down Expand Up @@ -269,58 +269,49 @@ func CheckInterfaceNameTypePhysical(name string) error {
return nil
}

func ExtractVlanTagFromName(name string) (vlanID int32, err error) {
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
// Extract the subinterface ID and bundle ID from an interface name.
// If the interface is a physical interface, the bundle ID will be 0 and the subinterface ID will be extracted if present.
// TwentyFiveGigE0/0/0/3.4095 -> (0, 4095), Bundle-Ether200 -> (200, 0), Bundle-Ether200.4095 -> (200, 4095)
func ExtractBundleAndSubinterfaceID(name string) (bundleID, subinterfaceID int32, err error) {
var idPart string
var typePhysical bool

switch {
case bundleEtherRE.MatchString(name):
idPart = strings.TrimPrefix(strings.TrimPrefix(name, "Bundle-Ether"), "bundle-ether")
case physicalInterfaceRE.MatchString(name):
idPart = name
typePhysical = true
default:
return 0, fmt.Errorf("unexpected interface name format %q, expected <interface> or <interface>.<vlan>", name)
return 0, 0, fmt.Errorf("interface name %q does not start with Bundle-Ether or bundle-ether or match physical interface pattern", name)
}
}

func ExtractBundleAndSubinterfaceID(name string) (bundleID, subinterfaceID int32, err error) {
// Extract bundle ID and optional subinterface ID from Bundle-Ether<id> or Bundle-Ether<id>.<subif_id>
// Examples: Bundle-Ether200 -> (200, 0), Bundle-Ether200.4095 -> (200, 4095)

// Remove the "Bundle-Ether" or "BE" prefix
var idPart string
parts := strings.Split(idPart, ".")

if !bundleEtherRE.MatchString(name) {
return 0, 0, fmt.Errorf("interface name %q does not start with Bundle-Ether or bundle-ether", name)
if len(parts) > 2 || len(parts) < 1 {
return 0, 0, fmt.Errorf("failed to extract subinterface ID from interface name %q", name)
}
idPart = strings.TrimPrefix(strings.TrimPrefix(name, "Bundle-Ether"), "bundle-ether")
parts := strings.Split(idPart, ".")

if len(parts) == 0 || parts[0] == "" {
return 0, 0, fmt.Errorf("failed to extract bundle ID from interface name %q", name)
if !typePhysical {
// For bundle interfaces, the first part is the bundle ID
bundleIDInt, err := strconv.ParseInt(parts[0], 10, 32)
if err != nil {
return 0, 0, fmt.Errorf("failed to parse bundle ID from interface name %q: %w", name, err)
}
bundleID = int32(bundleIDInt)
}

// Parse bundle ID
id, err := strconv.ParseInt(parts[0], 10, 32)
if err != nil {
return 0, 0, fmt.Errorf("failed to parse bundle ID from interface name %q: %w", name, err)
// If there is only one part, it means there is no subinterface ID specified, return 0 for subinterface ID
if len(parts) == 1 {
return bundleID, 0, nil
}
bundleID = int32(id)

// Parse subinterface ID if present
if len(parts) == 2 {
subIfaceIDInt, err := strconv.ParseInt(parts[1], 10, 32)
if err != nil {
return 0, 0, fmt.Errorf("failed to parse subinterface ID from interface name %q: %w", name, err)
}
subinterfaceID = int32(subIfaceIDInt)
} else if len(parts) > 2 {
return 0, 0, fmt.Errorf("unexpected interface name format %q, expected Bundle-Ether<id> or Bundle-Ether<id>.<subif_id>", name)
subIfaceID, err := strconv.ParseInt(parts[1], 10, 32)
if err != nil {
return 0, 0, fmt.Errorf("failed to parse subinterface ID from interface name %q: %w", name, err)
}

return bundleID, subinterfaceID, nil
return bundleID, int32(subIfaceID), nil
}

func CheckVlanRange(vlan string) error {
Expand Down
18 changes: 16 additions & 2 deletions internal/provider/cisco/iosxr/intf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,32 @@ func TestExtractBundleAndSubinterfaceID(t *testing.T) {
expectedSubIfaceID: 0,
wantErr: true,
},
{
name: "bundle-ether",
input: "bundle-ether200",
expectedBundleID: 200,
expectedSubIfaceID: 0,
wantErr: false,
},
{
name: "TenGigE",
input: "TenGigE0/0/0/1.100",
expectedBundleID: 0,
expectedSubIfaceID: 100,
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bundleID, subIfaceID, err := ExtractBundleAndSubinterfaceID(tt.input)
if tt.wantErr {
if err == nil {
t.Errorf("ExtractBundleAndSubinterfaceId(%s) expected error, got nil", tt.input)
t.Errorf("ExtractSubinterfaceIdFromInterfacenName(%s) expected error, got nil", tt.input)
}
} else {
if err != nil {
t.Errorf("ExtractBundleAndSubinterfaceId(%s) unexpected error: %v", tt.input, err)
t.Errorf("ExtractSubinterfaceIdFromInterfacenName(%s) unexpected error: %v", tt.input, err)
}
if bundleID != tt.expectedBundleID {
t.Errorf("ExtractBundleAndSubinterfaceId(%s) bundleID = %v, want %v", tt.input, bundleID, tt.expectedBundleID)
Expand Down
145 changes: 80 additions & 65 deletions internal/provider/cisco/iosxr/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,36 +150,14 @@ func (p *Provider) EnsureInterface(ctx context.Context, req *provider.EnsureInte
if bundleName == "" {
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"
}
// (fixme): support IPv6 addresses, IPv6 neighbor config

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 := prefix.Addr().String()
netmask := net.IP(net.CIDRMask(prefix.Bits(), 32)).String()

iface.IPv4Network = IPv4Network{
Addresses: AddressesIPv4{
Primary: Primary{
Address: ip,
Netmask: netmask,
},
},
ipv4, err := NewIPv4(req.Interface.Spec.IPv4)
if err != nil {
return err
}
iface.IPv4Network = ipv4
}

if req.Interface.Spec.MTU != 0 {
Expand Down Expand Up @@ -216,6 +194,7 @@ func (p *Provider) EnsureInterface(ctx context.Context, req *provider.EnsureInte
if req.Interface.Spec.AdminState == v1alpha1.AdminStateDown {
iface.Shutdown = gnmiext.Empty(true)
}
iface.Active = "act"
conf = append(conf, iface)

return updateInterface(ctx, p.client, conf...)
Expand All @@ -224,8 +203,8 @@ func (p *Provider) EnsureInterface(ctx context.Context, req *provider.EnsureInte
return err
}

iface := NewBundleInterface(req.Interface)

iface := NewInterface(name, req.Interface.Spec.Description)
iface.Active = "act"
bundleID, subinterfaceID, err := ExtractBundleAndSubinterfaceID(name)
if err != nil {
return err
Expand All @@ -251,51 +230,68 @@ func (p *Provider) EnsureInterface(ctx context.Context, req *provider.EnsureInte
},
}
conf = append(conf, &iface)
} else {
// (fixme): introduce new interface type subresource first. comes via different PR
return fmt.Errorf("subinterfaces for bundle interfaces are not supported yet: %q", name)

// Bundle subinterface configuration
// make sure the parent bundle-ether interface bundle-ether<id> exits
// parentBunndle := strings.Split(name, ".")[0]
// tmp := cp.Deep(req.Interface)
// tmp.Spec.Name = parentBunndle
// bundle := NewBundleInterface(tmp)
// conf = append(conf, &bundle)

// Unset for bundle subinterfaces
// iface.Mode = gnmiext.Empty(false)
// iface.ModeNoPhysical = "default"
// iface.SubInterface = VlanSubInterface{
// VlanIdentifier: VlanIdentifier{
// FirstTag: req.Interface.Spec.Switchport.AccessVlan,
// VlanType: "vlan-type-dot1q",
// },
// }

// Subinterface configures QAndQ vlan
// if req.Interface.Spec.Switchport.InnerVlan != 0 {
// iface.SubInterface.VlanIdentifier.SecondTag = req.Interface.Spec.Switchport.InnerVlan
// iface.SubInterface.VlanIdentifier.VlanType = "vlan-type-dot1ad"
// }
// conf = append(conf, &iface)
}
return updateInterface(ctx, p.client, conf...)
case v1alpha1.InterfaceTypeLoopback, v1alpha1.InterfaceTypeRoutedVLAN, v1alpha1.InterfaceTypeSubinterface:
case v1alpha1.InterfaceTypeSubinterface:
iface := NewInterface(name, req.Interface.Spec.Description)
// Set Interface mode to virtual for bundle interfaces
iface.Mode = gnmiext.Empty(false)
iface.ModeNoPhysical = "default"
iface.Active = "act"

_, subinterfaceID, err := ExtractBundleAndSubinterfaceID(name)
if err != nil {
return err
}

if subinterfaceID == 0 {
return fmt.Errorf("no subinterface ID in interfacename specified. pattern: <interface-name>.<subinterface-id>, got %q", name)
}

iface.SubInterface = VlanSubInterface{
VlanIdentifier: VlanIdentifier{
FirstTag: req.Interface.Spec.Encapsulation.OuterTag,
VlanType: "vlan-type-dot1q",
},
}

// Subinterface configures QAndQ vlan
if req.Interface.Spec.Encapsulation.InnerTag != 0 {
iface.SubInterface.VlanIdentifier.SecondTag = req.Interface.Spec.Encapsulation.InnerTag
iface.SubInterface.VlanIdentifier.VlanType = "vlan-type-dot1ad"
}

if req.Interface.Spec.MTU != 0 {
mtu, err := NewMTU(name, req.Interface.Spec.MTU)
if err != nil {
return err
}
iface.MTUs = mtu
}

if req.Interface.Spec.IPv4 != nil {
ipv4, err := NewIPv4(req.Interface.Spec.IPv4)
if err != nil {
return err
}
iface.IPv4Network = ipv4
}

conf = append(conf, &iface)
return updateInterface(ctx, p.client, conf...)
case v1alpha1.InterfaceTypeLoopback, v1alpha1.InterfaceTypeRoutedVLAN:
return fmt.Errorf("interface type %q is currently not supported", req.Interface.Spec.Type)
default:
return fmt.Errorf("unexpected interface type %q", req.Interface.Spec.Type)
}
}

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),
func NewInterface(name, descr string) Iface {
iface := Iface{
Name: name,
Description: descr,
}
return bundle
return iface
}

func NewVlanSubinterface(firstTag, secondTag int32, vlanType string) VlanSubInterface {
Expand All @@ -319,6 +315,25 @@ func NewMTU(intName string, mtu int32) (MTUs, error) {
}}}, nil
}

func NewIPv4(ips *v1alpha1.InterfaceIPv4) (IPv4Network, error) {
if ips == nil || len(ips.Addresses) == 0 {
return IPv4Network{}, errors.New("no IPv4 addresses configured for interface")
}

prefix := ips.Addresses[0]
ip := prefix.Addr().String()
netmask := net.IP(net.CIDRMask(prefix.Bits(), 32)).String()

return IPv4Network{
Addresses: AddressesIPv4{
Primary: Primary{
Address: ip,
Netmask: netmask,
},
},
}, nil
}

func updateInterface(ctx context.Context, client gnmiext.Client, conf ...gnmiext.DataElement) error {
for _, cf := range conf {
err := client.Update(ctx, cf)
Expand Down
Loading