Skip to main content

A modern CLI for managing a CNaaS-NMS deployment.

Project description

cnaas-nms-cli

A modern, batteries-included terminal client for CNaaS-NMS — the SUNET Campus-Network-as-a-Service controller.

Built on:

  • Typer — ergonomic, type-safe sub-commands with auto-generated --help
  • Rich — colored output, syntax-highlighted JSON, tables
  • cnaas-nms-api-client — generated HTTP client for the CNaaS-NMS REST API
  • Pydantic v2 — typed settings & validation
  • uv — fast, reproducible Python environments

cnaas-nms-cli lets you list and manage devices, drive ZTP and sync jobs, push firmware upgrades, inspect linknets/mgmtdomains/groups, refresh git repositories, query jobs and read settings — all from your shell.


Table of contents


Features

  • Hierarchical commands — 11 command groups with subcommands, each fully documented via --help.
  • Rich terminal UI — colored success/error output, syntax-highlighted JSON, optional table view for list commands.
  • Flexible config — env vars, .env files, persistent user config, CLI overrides, or interactive prompt fallback.
  • Friendly errors — server message payloads are surfaced cleanly; 401/403 hint at re-running cnaas auth configure; httpx network errors get a clean exit instead of a stack trace.
  • Test-first — 60 unit tests with typer.testing.CliRunner and a fake Response stub. No real HTTP calls during testing.
  • Modern Python — Python 3.10+, Pydantic v2, type-annotated end-to-end.

Requirements

  • Python ≥ 3.10
  • A reachable CNaaS-NMS server with API enabled
  • A bearer token (JWT) issued by your CNaaS-NMS deployment
  • uv for dependency management

Installation

git clone https://github.com/acidjunk/cnaas-nms-cli.git
cd cnaas-nms-cli

uv venv venv                           # create local venv at ./venv/
source venv/bin/activate
uv pip install -e ".[test,dev]"        # install + test/dev extras

After installation the cnaas script is on your PATH (inside the venv):

cnaas --version
# cnaas-nms-cli 0.1.0

Note: This project uses uv, never pip directly. All install/run/test workflows go through uv venv / uv pip / uv run.

Configuration

Environment variables

Variable Required Description
CNAAS_API_KEY yes Bearer token used to authenticate against the CNaaS API
CNAAS_BASE_URL yes Base URL of the CNaaS-NMS API (e.g. https://cnaas.example.org/api/v1.0)

A .env file in the current working directory is loaded automatically.

export CNAAS_BASE_URL="https://cnaas.example.org/api/v1.0"
export CNAAS_API_KEY="eyJhbGciOi..."

Persistent config file

Run cnaas auth configure to save credentials so you don't need to export them in every shell:

cnaas auth configure
# CNaaS base URL: https://cnaas.example.org/api/v1.0
# CNaaS API key: ********
# ✓ Credentials saved to /home/you/.config/cnaas-cli/.cnaas-cli.env

The file is written with mode 0600 and respects $XDG_CONFIG_HOME (defaults to ~/.config/cnaas-cli/). Show the path at any time with:

cnaas auth config-path

Per-invocation overrides

Two global options override env and config-file values just for one run:

cnaas --base-url https://staging.example.org/api/v1.0 \
      --api-key   eyJhbGciOi...                       \
      devices list

Precedence

When the CLI resolves a value it walks this list and stops at the first hit:

  1. --api-key / --base-url CLI options
  2. CNAAS_API_KEY / CNAAS_BASE_URL in the process environment
  3. .env in the current working directory
  4. $XDG_CONFIG_HOME/cnaas-cli/.cnaas-cli.env
  5. Interactive prompt (only if stdin and stdout are a TTY)
  6. Hard fail with exit 1 otherwise

This makes the CLI safe to use in CI: no value, no implicit prompt, no hang.

Interactive fallback

If you run cnaas in a terminal without env vars or a config file, you'll get prompted (the API key is masked):

$ cnaas system version
CNaaS base URL: https://cnaas.example.org/api/v1.0
CNaaS API key: ****************

Quick start

# Smoke-test connectivity
cnaas system version

# Inventory
cnaas devices list                              # Rich table
cnaas devices list -o json                      # raw JSON
cnaas devices show core-sw-01

# ZTP a freshly discovered device
cnaas devices init-check 42 --hostname core-sw-99 --device-type CORE
cnaas devices init       42 --hostname core-sw-99 --device-type CORE

# Sync a single device (dry run first!)
cnaas devices sync --hostname core-sw-01 --dry-run
cnaas devices sync --hostname core-sw-01

# Find your latest sync job
cnaas jobs list -o json
cnaas jobs show 1234

# Refresh templates after a git push
cnaas repository refresh templates

Output formats

list-style commands accept --output / -o:

Value Notes
table Default. Compact Rich table of common columns.
json Full server response, syntax-highlighted by Rich.

show-style commands always print syntax-highlighted JSON.

Command reference

This is a high-level overview. Run any command with --help for the full option list, including type and default value:

cnaas devices create --help

cnaas devices

Command Description
list List all devices known to CNaaS
show <hostname> Show one device
create <hostname> <device_type> Create a new device record (many optional fields)
delete <id> [--factory-default] Delete a device by numeric ID
init <id> --hostname --device-type Trigger ZTP/init
init-check <id> --hostname --device-type Pre-flight check before init
sync [--hostname/--group/--device-type/--all] [--dry-run] [--force] [--auto-push] [--resync] Push (or compute) configuration
generate-config <hostname> Render the candidate config from templates
running-config <hostname> Pull the live config off the device

cnaas linknets

Command Description
list [-o table|json] List all linknets
show <linknet_id> Show one linknet
create <device_a> <device_b> <port_a> <port_b> [--ipv4-network ...] Create a linknet
delete <linknet_id> Delete a linknet

cnaas mgmtdomains

Command Description
list [-o table|json] List all management domains
show <id> Show one mgmtdomain
create <device_a> <device_b> <vlan> <ipv4_gw> <ipv6_gw> [--description ...] Create a mgmtdomain
delete <id> Delete a mgmtdomain

cnaas groups

Command Description
list List all device groups
show <group_name> List members of a group
os-version <group> Show OS-version distribution within a group

cnaas interfaces

Command Description
list <hostname> List interfaces on a device
status <hostname> Show operational interface status
set-status <hostname> Bounce-down/bounce-up selected interfaces (templated)

cnaas firmware

Command Description
list List all firmware images
show <filename> Show one image
download --url --sha1 --filename [--no-verify-tls] Download to the CNaaS server
delete <filename> Delete an image
upgrade --url [--hostname / --group] [--filename] [--start-at] [--download] [--activate] [--pre-flight] [--post-flight] [--reboot] Trigger an upgrade

cnaas jobs

Command Description
list [-o ...] List recent jobs
show <id> Show details of one job
abort <id> Abort a running or scheduled job

cnaas repository

Command Description
show <repo> Show metadata for a managed git repo
refresh <repo> git pull the repo on the CNaaS server

Common values for <repo>: settings, templates, etc.

cnaas settings

Command Description
show [--hostname ...] [--device-type ...] Show effective settings (optionally filtered)
model Show the JSON-schema settings model
server Show server-side settings

cnaas system

Command Description
version Show CNaaS server version
shutdown [--yes] Gracefully shut the CNaaS API server down (admin only)

cnaas auth

Command Description
whoami Show the identity associated with the current API key
permissions Show the permissions granted by the current API key
refresh Refresh the current JWT (returns a new token)
configure Persist CNAAS_BASE_URL + CNAAS_API_KEY to the user config file
config-path Print the absolute path of the persistent config file

Shell completion

Typer ships completion for bash, zsh, fish and PowerShell out of the box:

cnaas --install-completion
# Or just print the script and add it manually:
cnaas --show-completion

Exit codes

Code Meaning
0 Success
1 API error (4xx/5xx), network error, missing config in non-interactive mode
2 Typer/CLI usage error (wrong flags, missing required argument)

Project layout

cnaas-nms-cli/
├── pyproject.toml
├── README.md
├── .gitignore
├── src/cnaas_cli/
│   ├── __init__.py             # __version__
│   ├── __main__.py             # python -m cnaas_cli
│   ├── main.py                 # root Typer app + global options
│   ├── config.py               # Pydantic settings + interactive prompt + persistent file
│   ├── client.py               # AuthenticatedClient factory (lru_cache)
│   ├── errors.py               # parse_response + handle_api_call context manager
│   ├── output.py               # Rich console, print_json, print_table, OutputFormat
│   └── commands/
│       ├── devices.py
│       ├── linknets.py
│       ├── mgmtdomains.py
│       ├── groups.py
│       ├── interfaces.py
│       ├── firmware.py
│       ├── jobs.py
│       ├── repository.py
│       ├── settings.py
│       ├── system.py
│       └── auth.py
└── tests/
    ├── conftest.py             # CliRunner fixture, FakeResponse, fake_client, env isolation
    ├── test_main.py
    ├── test_config.py
    ├── test_errors.py
    ├── test_devices.py
    ├── test_linknets.py
    ├── test_mgmtdomains.py
    ├── test_groups.py
    ├── test_interfaces.py
    ├── test_firmware.py
    ├── test_jobs.py
    ├── test_repository.py
    ├── test_settings.py
    ├── test_system.py
    └── test_auth.py

Development

uv venv venv
source venv/bin/activate
uv pip install -e ".[test,dev]"

uv run cnaas --help               # try the CLI
uv run pytest                     # run tests
uv run ruff check src tests       # lint
uv run ruff check --fix src tests # auto-fix lint

venv/ is gitignored.

Adding a new command

  1. Find the matching endpoint module under cnaas_nms_api_client/api/<resource>/<action>_api.py. The generated client exposes a sync_detailed(...) function for each one.

  2. Add a new @app.command(...) to the relevant src/cnaas_cli/commands/<group>.py, following the existing pattern:

    @app.command("foo")
    def foo(arg: str = typer.Argument(..., help="...")) -> None:
        """Short description shown in --help."""
        client = build_client()
        with handle_api_call("do foo"):
            response = some_endpoint_module.sync_detailed(arg, client=client)
            data = parse_response(response)
        print_json(data)
    
  3. Add a test in tests/test_<group>.py using stub_endpoint(...) from conftest.py. No real HTTP calls.

Testing

uv run pytest -q              # quick run
uv run pytest -v              # verbose
uv run pytest tests/test_devices.py::test_create_device
uv run pytest --cov=cnaas_cli --cov-report=term-missing

The test suite is hermetic: an autouse fixture sets fake env vars, clears the cached AuthenticatedClient, and isolates the persistent config file under a tmp_path XDG_CONFIG_HOME. Each test stubs the specific sync_detailed function it expects to be called and asserts on its arguments.

There are 60 tests at the moment, covering every command plus error paths (401 → exit 1 + auth hint, missing config in non-TTY → exit 1, validation errors on sync / firmware upgrade).

How it talks to CNaaS

cnaas-nms-api-client is an OpenAPI-generated client. Each REST endpoint lives in its own file and exposes:

def sync_detailed(*, client: AuthenticatedClient, ...) -> Response[Any]: ...

Two important quirks of the generated client influenced this CLI's design:

  1. response.parsed is always None for the endpoints we use — the actual JSON body sits in response.content (bytes). cnaas_cli.errors.parse_response decodes it and either returns the parsed value or raises CnaasCliError carrying the server's message field for clean rendering.
  2. POST/PUT/DELETE bodies are typed attrs models, not free dicts. The CLI constructs them from your CLI flags via Model(**{k: v for k, v in opts.items() if v is not None}).

Authentication uses a Bearer JWT in the Authorization header — see cnaas_nms_api_client.AuthenticatedClient.

Troubleshooting

Symptom Likely cause / fix
✗ Failed to ...: HTTP 401 and Authentication failed. hint Token is expired or wrong. Re-run cnaas auth configure.
✗ Missing CNAAS_BASE_URL ... in CI No env var, no .env, no TTY → set the env var explicitly in CI.
Network error while trying to ... The CNaaS server is unreachable / TLS error / DNS. Check --base-url.
Output is wrapped/garbled in a small terminal Force JSON: cnaas devices list -o json
command not found: cnaas The venv isn't on PATH. source venv/bin/activate or use uv run cnaas ….
Pre-existing token in the config file is being used Override with --api-key or unset CNAAS_API_KEY is not enough — the config file wins over an unset env. Edit/remove ~/.config/cnaas-cli/.cnaas-cli.env.

Releasing

Releases are published to PyPI automatically by .github/workflows/publish.yml using PyPI Trusted Publishing (OIDC) — no API tokens are stored in the repo.

One-time setup on PyPI:

  1. Create the project cnaas-nms-cli on PyPI (or use an existing one).
  2. Under the project's Publishing settings, add a new trusted publisher:
    • Owner: acidjunk
    • Repository: cnaas-nms-cli
    • Workflow name: publish.yml
    • Environment name: pypi
  3. In the GitHub repository settings, create an environment named pypi (Settings → Environments → New environment).

To cut a release:

# 1. Bump the version in pyproject.toml
$EDITOR pyproject.toml          # e.g. version = "0.2.0"

# 2. Commit and tag
git commit -am "Release v0.2.0"
git tag v0.2.0
git push origin main --tags

Pushing the v*.*.* tag triggers publish.yml, which:

  1. Verifies the tag matches pyproject.toml's version.
  2. Runs lint + tests.
  3. Builds sdist + wheel via uv build.
  4. Uploads them to PyPI via Trusted Publishing.
  5. Attaches the built distributions to a GitHub release with auto-generated notes.

You can also trigger the workflow manually from the Actions tab (workflow_dispatch) — useful for re-running after a PyPI hiccup. Manual runs skip the GitHub-release step (which is gated on the tag).

License

Apache-2.0 — see pyproject.toml. Same license as the upstream cnaas-nms-api-client and cnaas-nms projects.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cnaas_nms_cli-0.1.0.tar.gz (24.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cnaas_nms_cli-0.1.0-py3-none-any.whl (26.6 kB view details)

Uploaded Python 3

File details

Details for the file cnaas_nms_cli-0.1.0.tar.gz.

File metadata

  • Download URL: cnaas_nms_cli-0.1.0.tar.gz
  • Upload date:
  • Size: 24.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cnaas_nms_cli-0.1.0.tar.gz
Algorithm Hash digest
SHA256 12c8388a684e17043f6b2c954e6623bcc425ed9f8744ff4fbb6ed0c355584538
MD5 a8cad33e8d080962056facd274e27702
BLAKE2b-256 5328fac14c0eec59254c4dee230a4c54fb27218b5dbedb405ff2cb743117f51d

See more details on using hashes here.

Provenance

The following attestation bundles were made for cnaas_nms_cli-0.1.0.tar.gz:

Publisher: publish.yml on acidjunk/cnaas-nms-cli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cnaas_nms_cli-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: cnaas_nms_cli-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 26.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cnaas_nms_cli-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4d1d1466680eee0a71006d84faf4e94a294b99b008f23d89667a83a9f20829c7
MD5 9d511b662f5089b60cf5f7e3b530b6e5
BLAKE2b-256 59639a4ba91e0b2dad57dfa7fc24f971f60b99df2d8ff13401eefc9d9f130f0d

See more details on using hashes here.

Provenance

The following attestation bundles were made for cnaas_nms_cli-0.1.0-py3-none-any.whl:

Publisher: publish.yml on acidjunk/cnaas-nms-cli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page