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
10 changes: 7 additions & 3 deletions internal/deviceutil/deviceutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: we could just always use false here as this is only called with the insecure transport credentials.

// 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.
Expand Down
48 changes: 30 additions & 18 deletions internal/provider/cisco/gnmiext/v2/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,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 {
Expand All @@ -96,18 +108,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
}
}
Expand All @@ -117,34 +129,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.
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.
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.
// 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
}
Expand Down Expand Up @@ -179,7 +191,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
}
Expand Down Expand Up @@ -244,7 +256,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
}
Expand Down Expand Up @@ -293,7 +305,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 {
Expand All @@ -311,7 +323,7 @@ func (c *Client) Marshal(v any) (b []byte, err error) {
// 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.
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.
//
Expand Down Expand Up @@ -339,7 +351,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{
Expand All @@ -359,7 +371,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)
Expand Down
14 changes: 7 additions & 7 deletions internal/provider/cisco/gnmiext/v2/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand Down Expand Up @@ -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),
}
Expand Down Expand Up @@ -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),
}
Expand Down Expand Up @@ -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),
}
Expand Down Expand Up @@ -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),
}
Expand Down Expand Up @@ -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"},
Expand Down Expand Up @@ -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"},
Expand Down
136 changes: 136 additions & 0 deletions internal/provider/cisco/iosxr/intf.go
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

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

Note for later: Is this always set to active=act?

}

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 ExractMTUOwnerFromIfaceName(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)))
Comment on lines +96 to +98
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we use a strings.Split and split on the E? Or otherwise add a comment what the regex matches.


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)
}
Loading