Skip to content
Merged
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
105 changes: 105 additions & 0 deletions hack/nxapi/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors
// SPDX-License-Identifier: Apache-2.0

// nxapi is a minimal CLI tool for sending NX-API JSON-RPC commands to a
// Cisco NX-OS device and printing the results.
//
// Usage:
//
// go run ./hack/nxapi [flags] <cmd> [<cmd> ...]
//
// Example:
//
// go run ./hack/nxapi -address 10.0.0.1:443 "show version" "show interface brief"
package main

import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"os/signal"
"syscall"

"github.com/ironcore-dev/network-operator/internal/deviceutil"
"github.com/ironcore-dev/network-operator/internal/transport/nxapi"
)

var fs = flag.NewFlagSet("nxapi", flag.ContinueOnError)

func usage() {
fmt.Fprintf(os.Stderr, "Usage: nxapi [flags] <cmd> [<cmd> ...]\n\n")
fmt.Fprintf(os.Stderr, "Sends NX-API JSON-RPC commands to a Cisco NX-OS device.\n\n")
fmt.Fprintf(os.Stderr, "Flags:\n")
fs.PrintDefaults()
fmt.Fprintf(os.Stderr, "\nExample:\n")
fmt.Fprintf(os.Stderr, " nxapi -address 10.0.0.1:8080 \"show version\" \"show interface brief\"\n")
}

func main() {
fs.Usage = usage

address := fs.String("address", "localhost:8080", "device address (host:port)")
username := fs.String("username", "admin", "NX-API username")
password := fs.String("password", "admin", "NX-API password")

if err := fs.Parse(os.Args[1:]); err != nil {
// flag.ContinueOnError: -h/--help prints usage and returns ErrHelp;
// other parse errors are already printed by the FlagSet.
return
}

cmds := fs.Args()
if len(cmds) == 0 {
fs.Usage()
os.Exit(1)
}

conn := &deviceutil.Connection{
Address: *address,
Username: *username,
Password: *password,
}

ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt /* == syscall.SIGINT */, syscall.SIGTERM)
defer cancel()

c, err := nxapi.NewClient(conn, 0)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
return
}

res, err := c.Do(ctx, nxapi.NewRequest(cmds...))
if err != nil {
if errs, ok := errors.AsType[nxapi.RPCErrors](err); ok {
for _, e := range errs {
fmt.Fprintf(os.Stderr, "RPC error %d: %s\n", e.Code, e.Message)
if len(e.Data) > 0 {
fmt.Fprintf(os.Stderr, " data: %s\n", e.Data)
}
}
return
}
fmt.Fprintf(os.Stderr, "error: %v\n", err)
return
}

for i, r := range res {
fmt.Printf("=== [%d] %s ===\n", i+1, cmds[i])
var pretty any
if err := json.Unmarshal(r, &pretty); err != nil {
fmt.Println(string(r))
continue
}
out, err := json.MarshalIndent(pretty, "", " ")
if err != nil {
fmt.Fprintf(os.Stderr, "error: failed to pretty-print JSON: %v\n", err)
fmt.Println(string(r))
continue
}
fmt.Println(string(out))
}
}
18 changes: 16 additions & 2 deletions internal/provider/cisco/nxos/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/ironcore-dev/network-operator/internal/provider"
"github.com/ironcore-dev/network-operator/internal/transport/gnmiext"
"github.com/ironcore-dev/network-operator/internal/transport/grpcext"
"github.com/ironcore-dev/network-operator/internal/transport/nxapi"
)

var (
Expand Down Expand Up @@ -65,14 +66,17 @@ var (
type Provider struct {
conn *grpc.ClientConn
client gnmiext.Client
nxapi *nxapi.Client
}

func NewProvider() provider.Provider {
return &Provider{}
}

func (p *Provider) Connect(ctx context.Context, conn *deviceutil.Connection) (err error) {
p.conn, err = grpcext.NewClient(ctx, conn, grpcext.WithDefaultTimeout(30*time.Second))
// timeout is the default timeout for all HTTP/gRPC requests made by the provider.
const timeout = 30 * time.Second
p.conn, err = grpcext.NewClient(ctx, conn, grpcext.WithDefaultTimeout(timeout))
if err != nil {
return fmt.Errorf("failed to create grpc connection: %w", err)
}
Expand All @@ -81,7 +85,17 @@ func (p *Provider) Connect(ctx context.Context, conn *deviceutil.Connection) (er
opts = append(opts, gnmiext.WithLogger(logger))
}
p.client, err = gnmiext.New(ctx, p.conn, opts...)
return err
if err != nil {
return fmt.Errorf("failed to create gnmi client: %w", err)
}
// NXAPI only uses the address for URI construction.
c := *conn
c.Address = netip.MustParseAddrPort(conn.Address).String()
p.nxapi, err = nxapi.NewClient(&c, timeout)
if err != nil {
return fmt.Errorf("failed to create nxapi client: %w", err)
}
return nil
}

func (p *Provider) Disconnect(_ context.Context, _ *deviceutil.Connection) error {
Expand Down
18 changes: 18 additions & 0 deletions internal/transport/nxapi/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors
// SPDX-License-Identifier: Apache-2.0

// Package nxapi provides a JSON-RPC client for Cisco NX-OS devices via NX-API.
//
// Use [NewClient] to create a client for a given [deviceutil.Connection].
// The client supports both HTTP and HTTPS (selected automatically based on
// whether a TLS configuration is present) and authenticates with HTTP Basic Auth.
//
// Commands are expressed as plain NX-OS CLI strings and grouped into a [Request]
// using [NewRequest]. A single HTTP POST is made per [Request], which may contain
// one or more commands. Each command in a batch gets its own result in the
// returned slice.
//
// Errors are surfaced as [RPCErrors] when NX-OS reports one or more command
// failures, or as [HTTPError] for non-2xx responses whose body cannot be parsed
// as JSON-RPC (for example, a 401 Authorization Required).
package nxapi
Loading
Loading