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
6 changes: 6 additions & 0 deletions api/core/v1alpha1/interface_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please invoke make charts to also regenerate the Helm Chart incl. this CRD.

Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 16 additions & 8 deletions internal/provider/cisco/gnmiext/v2/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion internal/provider/cisco/gnmiext/v2/empty.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package gnmiext
import (
"encoding/json"
"fmt"
"regexp"
)

// NOTE: Use json.Marshaler and json.Unmarshaler interfaces instead of the
Expand Down Expand Up @@ -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]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Due to some Cisco IOSX ouptut we also match [ \n null \n]
// Due to some Cisco IOS-XR output we also match [ \n null \n]

nullTypeRe := regexp.MustCompile(`^\[\s*null\s*]$`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can you move this variable outside of the function, so that we don't recreated this regexp instance on every function call?

if !nullTypeRe.MatchString(string(b)) {
return fmt.Errorf("gnmiext: invalid empty value: %s", string(b))
}
*e = true
Expand Down
230 changes: 186 additions & 44 deletions internal/provider/cisco/iosxr/intf.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo


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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Represent physical and bundle interfaces as part of the same struct as they share a lot of common configuration
// Iface represents physical and bundle interfaces as part of the same struct as they share a lot of common configuration

Type comments should start with the type name.

// 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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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 <port_type>/<rack>/<slot/<module>/<port>
// 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<VLAN>[.<VLAN>] or BE<VLAN>[.<VLAN>]
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 <interface> or <interface>.<vlan>", 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
}
2 changes: 1 addition & 1 deletion internal/provider/cisco/iosxr/intf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ func init() {
Owner: "TwentyFiveGigE",
}

Register("intf", &PhysIf{
Register("intf", &Iface{
Name: name,
Description: "random interface test",
Active: "act",
Expand Down
Loading
Loading