diff --git a/.typos.toml b/.typos.toml index 6737dd25..e9a27abc 100644 --- a/.typos.toml +++ b/.typos.toml @@ -1,5 +1,9 @@ # SPDX-FileCopyrightText: 2026 SAP SE # SPDX-License-Identifier: Apache-2.0 +[default] +extend-ignore-re = [ + "Cisco-IOS-XR.*" +] [default.extend-words] ser = "ser" diff --git a/internal/deviceutil/deviceutil.go b/internal/deviceutil/deviceutil.go index 3c9dc07e..95a0486e 100644 --- a/internal/deviceutil/deviceutil.go +++ b/internal/deviceutil/deviceutil.go @@ -164,8 +164,9 @@ func WithDefaultTimeout(timeout time.Duration) Option { } type auth struct { - Username string - Password string + Username string + Password string + SecureTransportCreds bool } var _ credentials.PerRPCCredentials = (*auth)(nil) @@ -177,7 +178,10 @@ func (a *auth) GetRequestMetadata(_ context.Context, _ ...string) (map[string]st }, nil } -func (a *auth) RequireTransportSecurity() bool { return true } +func (a *auth) RequireTransportSecurity() bool { + // Only called if the transport credentials are insecure. + return false +} // UnaryDefaultTimeoutInterceptor returns a gRPC unary client interceptor that sets a default timeout // for each RPC. If a deadline is already present , it will not be modified. diff --git a/internal/provider/cisco/gnmiext/v2/client.go b/internal/provider/cisco/gnmiext/v2/client.go index 9a661e2f..73fc2f8d 100644 --- a/internal/provider/cisco/gnmiext/v2/client.go +++ b/internal/provider/cisco/gnmiext/v2/client.go @@ -56,21 +56,33 @@ type Capabilities struct { SupportedModels []Model } +type Client interface { + GetState(ctx context.Context, conf ...Configurable) error + GetConfig(ctx context.Context, conf ...Configurable) error + Patch(ctx context.Context, conf ...Configurable) error + Update(ctx context.Context, conf ...Configurable) error + Delete(ctx context.Context, conf ...Configurable) error +} + // Client is a gNMI client offering convenience methods for device configuration // using gNMI. -type Client struct { +type client struct { gnmi gpb.GNMIClient encoding gpb.Encoding capabilities *Capabilities logger logr.Logger } +var ( + _ Client = &client{} +) + // New creates a new Client by negotiating capabilities with the gNMI server by // carrying out a Capabilities RPC. // Returns an error if the device doesn't support JSON encoding. // By default, the client uses [slog.Default] for logging. // Use [WithLogger] to provide a custom logger. -func New(ctx context.Context, conn grpc.ClientConnInterface, opts ...Option) (*Client, error) { +func New(ctx context.Context, conn grpc.ClientConnInterface, opts ...Option) (Client, error) { gnmi := gpb.NewGNMIClient(conn) res, err := gnmi.Capabilities(ctx, &gpb.CapabilityRequest{}) if err != nil { @@ -97,18 +109,18 @@ func New(ctx context.Context, conn grpc.ClientConnInterface, opts ...Option) (*C } } logger := logr.FromSlogHandler(slog.Default().Handler()) - client := &Client{gnmi, encoding, capabilities, logger} + c := &client{gnmi, encoding, capabilities, logger} for _, opt := range opts { - opt(client) + opt(c) } - return client, nil + return c, nil } -type Option func(*Client) +type Option func(*client) // WithLogger sets a custom logger for the client. func WithLogger(logger logr.Logger) Option { - return func(c *Client) { + return func(c *client) { c.logger = logger } } @@ -118,36 +130,34 @@ var ErrNil = errors.New("gnmiext: nil") // GetConfig retrieves config and unmarshals it into the provided targets. // If some of the values for the given xpaths are not defined, [ErrNil] is returned. -// Fields that are not present in the response are set to their zero value. -func (c *Client) GetConfig(ctx context.Context, conf ...Configurable) error { +func (c *client) GetConfig(ctx context.Context, conf ...Configurable) error { return c.get(ctx, gpb.GetRequest_CONFIG, conf...) } // GetState retrieves state and unmarshals it into the provided targets. // If some of the values for the given xpaths are not defined, [ErrNil] is returned. -// Fields that are not present in the response are set to their zero value. -func (c *Client) GetState(ctx context.Context, conf ...Configurable) error { +func (c *client) GetState(ctx context.Context, conf ...Configurable) error { return c.get(ctx, gpb.GetRequest_STATE, conf...) } -// Update replaces the configuration for the given set of items. +// Update replaces the configuration for the given set of items.4c890d // If the current configuration equals the desired configuration, the operation is skipped. // For partial updates that merge changes, use [Client.Patch] instead. -func (c *Client) Update(ctx context.Context, conf ...Configurable) error { +func (c *client) Update(ctx context.Context, conf ...Configurable) error { return c.set(ctx, false, conf...) } // Patch merges the configuration for the given set of items. // If the current configuration equals the desired configuration, the operation is skipped. // For full replacement of configuration, use [Client.Update] instead. -func (c *Client) Patch(ctx context.Context, conf ...Configurable) error { +func (c *client) Patch(ctx context.Context, conf ...Configurable) error { return c.set(ctx, true, conf...) } // Delete resets the configuration for the given set of items. // If an item implements [Defaultable], it's reset to default value. // Otherwise, the configuration is deleted. -func (c *Client) Delete(ctx context.Context, conf ...Configurable) error { +func (c *client) Delete(ctx context.Context, conf ...Configurable) error { if len(conf) == 0 { return nil } @@ -182,7 +192,7 @@ func (c *Client) Delete(ctx context.Context, conf ...Configurable) error { // get retrieves data of the specified type (CONFIG or STATE) and unmarshals it // into the provided targets. If some of the values for the given xpaths are not // defined, [ErrNil] is returned. -func (c *Client) get(ctx context.Context, dt gpb.GetRequest_DataType, conf ...Configurable) error { +func (c *client) get(ctx context.Context, dt gpb.GetRequest_DataType, conf ...Configurable) error { if len(conf) == 0 { return nil } @@ -247,7 +257,7 @@ func (c *Client) get(ctx context.Context, dt gpb.GetRequest_DataType, conf ...Co // configuration. Otherwise, a full replacement is done. // If the current configuration equals the desired configuration, the operation // is skipped. -func (c *Client) set(ctx context.Context, patch bool, conf ...Configurable) error { +func (c *client) set(ctx context.Context, patch bool, conf ...Configurable) error { if len(conf) == 0 { return nil } @@ -296,7 +306,7 @@ func (c *Client) set(ctx context.Context, patch bool, conf ...Configurable) erro // Marshal marshals the provided value into a byte slice using the client's encoding. // If the value implements the [Marshaler] interface, it will be marshaled using that. // Otherwise, [json.Marshal] is used. -func (c *Client) Marshal(v any) (b []byte, err error) { +func (c *client) Marshal(v any) (b []byte, err error) { if m, ok := v.(Marshaler); ok { b, err = m.MarshalYANG(c.capabilities) if err != nil { @@ -341,9 +351,7 @@ func zeroUnknownFields(b []byte, rv reflect.Value) { // Unmarshal unmarshals the provided byte slice into the provided destination. // If the destination implements the [Marshaler] interface, it will be unmarshaled using that. // Otherwise, [json.Unmarshal] is used. -// Additionally, if will ensure that fields not present in the JSON -// are set to their zero value. -func (c *Client) Unmarshal(b []byte, dst any) (err error) { +func (c *client) Unmarshal(b []byte, dst any) (err error) { // NOTE: If you query for list elements on Cisco NX-OS, the encoded payload // will be the wrapped in an array (even if only one element is requested), i.e. // @@ -372,7 +380,7 @@ func (c *Client) Unmarshal(b []byte, dst any) (err error) { } // Encode encodes the provided byte slice into a [gpb.TypedValue] using the client's encoding. -func (c *Client) Encode(b []byte) *gpb.TypedValue { +func (c *client) Encode(b []byte) *gpb.TypedValue { switch c.encoding { case gpb.Encoding_JSON: return &gpb.TypedValue{ @@ -392,7 +400,7 @@ func (c *Client) Encode(b []byte) *gpb.TypedValue { } // Decode decodes the provided [gpb.TypedValue] into the provided destination using the client's encoding. -func (c *Client) Decode(val *gpb.TypedValue) ([]byte, error) { +func (c *client) Decode(val *gpb.TypedValue) ([]byte, error) { switch c.encoding { case gpb.Encoding_JSON: v, ok := val.Value.(*gpb.TypedValue_JsonVal) diff --git a/internal/provider/cisco/gnmiext/v2/client_test.go b/internal/provider/cisco/gnmiext/v2/client_test.go index f5082b4f..cfcc1fa6 100644 --- a/internal/provider/cisco/gnmiext/v2/client_test.go +++ b/internal/provider/cisco/gnmiext/v2/client_test.go @@ -502,7 +502,7 @@ func TestClient_GetConfig(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - client := &Client{ + client := &client{ encoding: gpb.Encoding_JSON, gnmi: gpb.NewGNMIClient(test.conn), } @@ -582,7 +582,7 @@ func TestClient_GetState(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - client := &Client{ + client := &client{ encoding: gpb.Encoding_JSON, gnmi: gpb.NewGNMIClient(test.conn), } @@ -853,7 +853,7 @@ func TestClient_Update(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - client := &Client{ + client := &client{ encoding: gpb.Encoding_JSON, gnmi: gpb.NewGNMIClient(test.conn), } @@ -1015,7 +1015,7 @@ func TestClient_Patch(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - client := &Client{ + client := &client{ encoding: gpb.Encoding_JSON_IETF, gnmi: gpb.NewGNMIClient(test.conn), } @@ -1133,7 +1133,7 @@ func TestClient_Delete(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - client := &Client{ + client := &client{ encoding: gpb.Encoding_JSON, gnmi: gpb.NewGNMIClient(test.conn), } @@ -1231,7 +1231,7 @@ func TestClient_Marshal(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - client := &Client{ + client := &client{ capabilities: &Capabilities{ SupportedModels: []Model{ {Name: "openconfig-interfaces", Organization: "OpenConfig working group", Version: "2.5.0"}, @@ -1288,7 +1288,7 @@ func TestClient_Unmarshal(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - client := &Client{ + client := &client{ capabilities: &Capabilities{ SupportedModels: []Model{ {Name: "openconfig-interfaces", Organization: "OpenConfig working group", Version: "2.5.0"}, diff --git a/internal/provider/cisco/iosxr/intf.go b/internal/provider/cisco/iosxr/intf.go new file mode 100644 index 00000000..76045b68 --- /dev/null +++ b/internal/provider/cisco/iosxr/intf.go @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package iosxr + +import ( + "fmt" + "regexp" + + "github.com/ironcore-dev/network-operator/internal/provider/cisco/gnmiext/v2" +) + +type PhysIf 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"` +} + +type Statistics struct { + LoadInterval uint8 `json:"load-interval"` +} + +type IPv4Network struct { + Addresses AddressesIPv4 `json:"addresses"` + Mtu uint16 `json:"mtu"` +} + +type AddressesIPv4 struct { + Primary Primary `json:"primary"` +} + +type Primary struct { + Address string `json:"address"` + Netmask string `json:"netmask"` +} + +type IPv6Network struct { + Mtu uint16 `json:"mtu"` + Addresses AddressesIPv6 `json:"addresses"` +} + +type AddressesIPv6 struct { + RegularAddresses RegularAddresses `json:"regular-addresses"` +} + +type RegularAddresses struct { + RegularAddress []RegularAddress `json:"regular-address"` +} + +type RegularAddress struct { + Address string `json:"address"` + PrefixLength uint8 `json:"prefix-length"` + Zone string `json:"zone"` +} + +type IPv6Neighbor struct { + RASuppress bool `json:"ra-suppress"` +} + +type MTUs struct { + MTU []MTU `json:"mtu"` +} + +type MTU struct { + MTU int32 `json:"mtu"` + Owner string `json:"owner"` +} + +func (i *PhysIf) 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) +} + +type IFaceSpeed string + +const ( + Speed10G IFaceSpeed = "TenGigE" + Speed25G IFaceSpeed = "TwentyFiveGigE" + Speed40G IFaceSpeed = "FortyGigE" + Speed100G IFaceSpeed = "HundredGigE" +) + +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) + } + + switch mtuOwner { + case string(Speed10G): + return Speed10G, nil + case string(Speed25G): + return Speed25G, nil + case string(Speed40G): + return Speed25G, nil + case string(Speed100G): + return Speed100G, nil + default: + return "", fmt.Errorf("unsupported interface type %s for MTU owner extraction", mtuOwner) + } +} + +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" +) + +type PhysIfState struct { + State string `json:"state"` + Name string `json:"-"` +} + +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) +} diff --git a/internal/provider/cisco/iosxr/intf_test.go b/internal/provider/cisco/iosxr/intf_test.go new file mode 100644 index 00000000..c5cc84d3 --- /dev/null +++ b/internal/provider/cisco/iosxr/intf_test.go @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package iosxr + +func init() { + name := "TwentyFiveGigE0/0/0/14" + + mtu := MTU{ + MTU: 9026, + Owner: "TwentyFiveGigE", + } + + Register("intf", &PhysIf{ + Name: name, + Description: "random interface test", + Active: "act", + Vrf: "default", + Statistics: Statistics{ + LoadInterval: 30, + }, + MTUs: MTUs{ + []MTU{mtu}, + }, + Shutdown: true, + IPv4Network: IPv4Network{ + Addresses: AddressesIPv4{ + Primary: Primary{ + Address: "192.168.1.2", + Netmask: "255.255.255.0", + }, + }, + Mtu: 1000, + }, + IPv6Network: IPv6Network{ + Mtu: 2100, + Addresses: AddressesIPv6{ + RegularAddresses: RegularAddresses{ + RegularAddress: []RegularAddress{ + { + Address: "2001:db8::1", + PrefixLength: 64, + Zone: "", + }, + }, + }, + }, + }, + }) +} diff --git a/internal/provider/cisco/iosxr/provider.go b/internal/provider/cisco/iosxr/provider.go new file mode 100644 index 00000000..1d58a522 --- /dev/null +++ b/internal/provider/cisco/iosxr/provider.go @@ -0,0 +1,166 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package iosxr + +import ( + "context" + "errors" + "fmt" + "strconv" + + "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" +) + +var ( + _ provider.Provider = &Provider{} + _ provider.InterfaceProvider = &Provider{} +) + +type Provider struct { + conn *grpc.ClientConn + client gnmiext.Client +} + +func NewProvider() provider.Provider { + return &Provider{} +} + +func (p *Provider) Connect(ctx context.Context, conn *deviceutil.Connection) (err error) { + p.conn, err = deviceutil.NewGrpcClient(ctx, conn) + if err != nil { + return fmt.Errorf("failed to create grpc connection: %w", err) + } + p.client, err = gnmiext.New(ctx, p.conn) + if err != nil { + return err + } + return nil +} + +func (p *Provider) Disconnect(ctx context.Context, conn *deviceutil.Connection) error { + return p.conn.Close() +} + +func (p *Provider) EnsureInterface(ctx context.Context, req *provider.EnsureInterfaceRequest) error { + if p.client == nil { + 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 + + physif := &PhysIf{} + + physif.Name = req.Interface.Spec.Name + physif.Description = req.Interface.Spec.Description + + 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) + + if len(req.Interface.Spec.IPv4.Addresses) == 0 { + message := "no IPv4 address configured for interface " + name + return errors.New(message) + } + + 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 + 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), + }, + }, + } + + // Check if interface exists otherwise patch will fail + tmpPhysif := &PhysIf{} + tmpPhysif.Name = name + + err = p.client.GetConfig(ctx, tmpPhysif) + 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 + } + + err = p.client.Update(ctx, physif) + if err != nil { + return err + } + + return nil +} + +func (p *Provider) DeleteInterface(ctx context.Context, req *provider.InterfaceRequest) error { + physif := &PhysIf{} + physif.Name = req.Interface.Spec.Name + + if p.client == nil { + return errors.New("client is not connected") + } + + err := p.client.Delete(ctx, physif) + if err != nil { + return fmt.Errorf("failed to delete interface %s: %w", req.Interface.Spec.Name, err) + } + return nil +} + +func (p *Provider) GetInterfaceStatus(ctx context.Context, req *provider.InterfaceRequest) (provider.InterfaceStatus, error) { + state := new(PhysIfState) + state.Name = req.Interface.Spec.Name + + if p.client == nil { + return provider.InterfaceStatus{}, errors.New("client is not connected") + } + + err := p.client.GetState(ctx, state) + + if err != nil { + return provider.InterfaceStatus{}, fmt.Errorf("failed to get interface status for %s: %w", req.Interface.Spec.Name, err) + } + + return provider.InterfaceStatus{ + OperStatus: state.State == string(StateUp), + }, nil +} + +func init() { + provider.Register("cisco-iosxr-gnmi", NewProvider) +} diff --git a/internal/provider/cisco/iosxr/provider_test.go b/internal/provider/cisco/iosxr/provider_test.go new file mode 100644 index 00000000..0ec09c39 --- /dev/null +++ b/internal/provider/cisco/iosxr/provider_test.go @@ -0,0 +1,207 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package iosxr + +import ( + "bytes" + "context" + "encoding/json" + "net/netip" + "os" + "strings" + "testing" + + "github.com/tidwall/gjson" + + "github.com/ironcore-dev/network-operator/api/core/v1alpha1" + "github.com/ironcore-dev/network-operator/internal/provider" + "github.com/ironcore-dev/network-operator/internal/provider/cisco/gnmiext/v2" +) + +type TestCase struct { + name string + val gnmiext.Configurable +} + +var tests []TestCase + +func Register(name string, val gnmiext.Configurable) { + tests = append(tests, TestCase{ + name: name, + val: val, + }) +} + +func removeRootElement(xpath string) string { + parts := strings.Split(xpath, "/") + if len(parts) == 1 { + return xpath + } + return strings.Join(parts[1:], "/") +} + +func Test_Payload(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + b, err := json.Marshal(test.val) + if err != nil { + t.Errorf("json.Marshal() error = %v", err) + return + } + + file := "testdata/" + test.name + ".json" + data, err := os.ReadFile(file) + if err != nil { + t.Fatalf("os.ReadFile(%q) error = %v", file, err) + } + + var buf bytes.Buffer + if err := json.Compact(&buf, data); err != nil { + t.Errorf("json.Compact() error = %v", err) + return + } + + xpath := removeRootElement(test.val.XPath()) + path, err := gnmiext.StringToStructuredPath(xpath) + if err != nil { + t.Errorf("StringToStructuredPath(%q) error = %v", xpath, err) + return + } + + var sb strings.Builder + for _, elem := range path.GetElem() { + if elem.GetName() == "" { + continue + } + if sb.Len() > 0 { + sb.WriteByte('|') + } + sb.WriteString(elem.GetName()) + } + + res := gjson.GetBytes(buf.Bytes(), sb.String()) + if want := []byte(res.Raw); !bytes.Equal(want, b) { + t.Errorf("payload mismatch:\nwant: %s\ngot: %s", want, b) + } + }) + } +} + +// MockClient provides a mock implementation of gnmiext.Client for testing. +type MockClient struct { + // Function fields for mocking different methods + GetConfigFunc func(ctx context.Context, conf ...gnmiext.Configurable) error + PatchFunc func(ctx context.Context, conf ...gnmiext.Configurable) error + UpdateFunc func(ctx context.Context, conf ...gnmiext.Configurable) error + DeleteFunc func(ctx context.Context, conf ...gnmiext.Configurable) error + GetStateFunc func(ctx context.Context, conf ...gnmiext.Configurable) error +} + +// Implement the methods that Provider uses +func (m *MockClient) GetConfig(ctx context.Context, conf ...gnmiext.Configurable) error { + if m.GetConfigFunc != nil { + return m.GetConfigFunc(ctx, conf...) + } + return nil +} + +func (m *MockClient) Patch(ctx context.Context, conf ...gnmiext.Configurable) error { + return nil +} + +func (m *MockClient) Update(ctx context.Context, conf ...gnmiext.Configurable) error { + return nil +} + +func (m *MockClient) Delete(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...) + } + return nil +} + +func Test_EnsureInterface(t *testing.T) { + m := &MockClient{} + + p := &Provider{ + client: m, + conn: nil, + } + + ctx := context.Background() + + var name = "TwentyFiveGigE0/0/0/14" + var prefix netip.Prefix + + prefix, err := netip.ParsePrefix("192.168.1.0/24") + + if err != nil { + t.Fatalf("Failed to parse prefix: %v", err) + } + + ipv4 := v1alpha1.InterfaceIPv4{ + Addresses: []v1alpha1.IPPrefix{ + { + Prefix: prefix, + }, + }, + } + + req := &provider.EnsureInterfaceRequest{ + Interface: &v1alpha1.Interface{ + Spec: v1alpha1.InterfaceSpec{ + Name: name, + IPv4: &ipv4, + Description: "i572056-test-2", + AdminState: "UP", + Type: "Physical", + MTU: 9600, + }, + }, + } + + err = p.EnsureInterface(ctx, req) + if err != nil { + t.Fatalf("EnsureInterface() error = %v", err) + } +} + +func Test_GetState(t *testing.T) { + m := &MockClient{ + GetStateFunc: func(ctx context.Context, conf ...gnmiext.Configurable) error { + conf[0].(*PhysIfState).State = "im-state-up" + return nil + }, + } + + p := &Provider{ + client: m, + conn: nil, + } + + ctx := context.Background() + + var name = "TwentyFiveGigE0/0/0/14" + + req := &provider.InterfaceRequest{ + Interface: &v1alpha1.Interface{ + Spec: v1alpha1.InterfaceSpec{ + Name: name, + }, + }, + } + + status, err := p.GetInterfaceStatus(ctx, req) + if err != nil { + t.Fatalf("EnsureInterface() error = %v", err) + } + + if status.OperStatus != true { + t.Fatalf("GetInterfaceStatus() expected OperStatus=true, got false") + } +} diff --git a/internal/provider/cisco/iosxr/testdata/intf.json b/internal/provider/cisco/iosxr/testdata/intf.json new file mode 100644 index 00000000..ccef73d4 --- /dev/null +++ b/internal/provider/cisco/iosxr/testdata/intf.json @@ -0,0 +1,45 @@ +{ + "interface-configuration": { + "description": "random interface test", + "active": "act", + "Cisco-IOS-XR-infra-rsi-cfg:vrf": "default", + "Cisco-IOS-XR-infra-statsd-cfg:statistics": { + "load-interval": 30 + }, + "Cisco-IOS-XR-ipv4-io-cfg:ipv4-network": { + "addresses": { + "primary": { + "address": "192.168.1.2", + "netmask": "255.255.255.0" + } + }, + "mtu": 1000 + }, + "Cisco-IOS-XR-ipv6-ma-cfg:ipv6-network": { + "mtu": 2100, + "addresses": { + "regular-addresses": { + "regular-address": [ + { + "address": "2001:db8::1", + "prefix-length": 64, + "zone": "" + } + ] + } + } + }, + "Cisco-IOS-XR-ipv6-nd-cfg:ipv6-neighbor": { + "ra-suppress": false + }, + "mtus": { + "mtu": [ + { + "mtu": 9026, + "owner": "TwentyFiveGigE" + } + ] + }, + "shutdown": [null] + } +} diff --git a/internal/provider/cisco/iosxr/testdata/intf.json.txt b/internal/provider/cisco/iosxr/testdata/intf.json.txt new file mode 100644 index 00000000..80e80638 --- /dev/null +++ b/internal/provider/cisco/iosxr/testdata/intf.json.txt @@ -0,0 +1,5 @@ +interface TwentyFiveGigE0/0/0/14 + description test + mtu 9026 + ipv4 address 192.168.1.2 255.255.255.0 + load-interval 30 diff --git a/internal/provider/cisco/nxos/intf.go b/internal/provider/cisco/nxos/intf.go index a79cab8d..d72d17e1 100644 --- a/internal/provider/cisco/nxos/intf.go +++ b/internal/provider/cisco/nxos/intf.go @@ -373,7 +373,7 @@ func Range(r []int32) string { } // Exists checks if all provided interface names exist on the device. -func Exists(ctx context.Context, client *gnmiext.Client, names ...string) (bool, error) { +func Exists(ctx context.Context, client gnmiext.Client, names ...string) (bool, error) { if len(names) == 0 { return false, errors.New("at least one interface name must be provided") } diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index d89057a7..1f189942 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -62,7 +62,7 @@ var ( type Provider struct { conn *grpc.ClientConn - client *gnmiext.Client + client gnmiext.Client } func NewProvider() provider.Provider { diff --git a/typosconf.toml b/typosconf.toml new file mode 100644 index 00000000..d0c392af --- /dev/null +++ b/typosconf.toml @@ -0,0 +1,4 @@ +[default] +extend-ignore-re = [ + "Cisco-IOS-XR.*" +]