axlib 1.0.7
Pages and sections ↓

axlib release notes#

Newest release first. CHANGELOG.md lists every individual change.

1.0.7#

Overview#

This release adds axlib md2html, which turns Markdown runbooks, change plans, and notes into HTML pages, and uses it to publish the documentation as a website: https://robert-patrick-texas.github.io/axlib/. Every guide was checked against the code and corrected, and the bugs found along the way are fixed. One output changes on purpose: axlib netenv-set no longer prints unset NETENABLE unless asked to (see Compatibility below).

Markdown to HTML#

axlib md2html change-plan.md                  # writes change-plan.html beside it
axlib md2html -r runbooks -o site --index runbooks/README.md --site-name "NOC runbooks"

docs/MARKDOWN.md is the guide.

Documentation website#

The guides are now published at https://robert-patrick-texas.github.io/axlib/, with the GitHub repository and the PyPI project linked at the top of every page and an index of pages and sections down the left. make docs builds the same site locally, and .github/workflows/docs.yml publishes it from the tag of every GitHub release, so the website always describes the latest release.

Every guide was checked against the code, and checked again after the corrections. Among the corrections: the recovery advice after an interrupted rotate-key, which could have deleted the only working key; when ax.getkeys() falls back to the shared account; and how install.sh --no-store behaves. New material includes a reference of every axlib.toml setting and AXLIB_* variable, the Redis cache, both stores' command lines, the full Python APIs, and a table of every command in the README.

Fixes#

Compatibility#

Validation#

Validated on Python 3.14.3 and, from the built wheel, on Python 3.11.16:

Ruff format and lint (all rules, including docs code): passed
ty static type check:                                  passed
Educational docstring audit:                           passed
Main pytest suite:                                     453 passed
Full suite from the built wheel, Python 3.11 and 3.14: 453 passed
Documentation site (make docs --strict):               no broken links
Installer in an Ubuntu 26.04 container, from the
  release tarballs: 1.0.6 with the TUI, parsing, and
  the login snippet upgraded to 1.0.7 (options,
  configuration, and records kept; login snippet
  passes --unset), netenv-set output with and without
  --unset, --with-markdown added and the release's
  docs built by an operator, the documented rollback
  to 1.0.6, and the upgrade again:                     passed
Installer failure paths (failed uv add and failed
  build restore the previous state):                   passed
shellcheck of install.sh, axuv, and netenv.sh:         passed

1.0.6#

Overview#

This release adds show-command parsing. axlib parse turns the output of commands such as show cdp neighbors into records with TextFSM templates: the ntc-templates collection plus the team's own .textfsm files. axlib parse diff compares a collection taken before a maintenance window with one taken after it, record by record. Neighbors listed in a different order and hold times that ticked down are not reported. Removed neighbors, moved ports, and changed fields are. Parsing is an optional install, uv add 'axlib[textfsm]', or install.sh --with-textfsm on a shared host. Nothing else changes.

Before and after a change#

axlib parse -p cisco_ios -r captures/before/ -o before.json
axlib parse diff -p cisco_ios before.json captures/after/
sw1: show cdp neighbors
  - local_interface=GigabitEthernet1/0/5 neighbor_name=ap-lobby-01
  ~ local_interface=GigabitEthernet1/0/48 neighbor_name=core-rtr-01.example.net
      neighbor_interface: GigabitEthernet0/0/1 -> GigabitEthernet0/0/2
sw1: show ip pim neighbor
  ~ neighbor=10.20.0.2 interface=Vlan20
      mode: "DR S P G" -> "S P G"
...
9 commands compared: 7 changed, 0 only before, 0 only after, 2 unchanged

On the sample captures in examples/parse, diff -u of one switch's logs shows 110 changed lines and of the other's 6. axlib parse diff reports four real changes on the first switch and nothing on the second, whose neighbors were only listed in another order.

The commands are also available as axlib-parse and python -m axlib.parse. docs/PARSING.md is the operator guide.

The team's own templates#

A template needs only a file name: put <platform>_<command>.textfsm, such as cisco_ios_show_ip_pim_neighbor.textfsm, in a template directory. No index file is needed; commands with arguments still need an ntc-templates style index. Directories are searched in this order:

  1. --template-dir DIR (repeatable)
  2. AXLIB_TEXTFSM_TEMPLATES
  3. template_dirs in the new [textfsm] table of axlib.toml, or textfsm/ beside axlib.toml when it has none. On a shared host this is /etc/axlib/textfsm.
  4. ntc-templates, or NTC_TEMPLATES_DIR

A template with the same name as an ntc template replaces it, however the command is abbreviated in the capture. axlib parse templates -p cisco_ios sh ip pim nei shows which template parses a command and where it came from. [textfsm] can also set a default platform, as can AXLIB_TEXTFSM_PLATFORM.

Python API#

from axlib.parse import parse_output

records = parse_output(
    conn.send_command("show cdp neighbors"), "show cdp neighbors", "cisco_ios"
)

parse_output() suits netmiko and scrapli scripts. parse_text(), parse_paths(), and compare() do what the commands do, and TemplateCatalog, split_capture(), canonical_interface(), and normalize_records() are public for scripts that need one step.

Shared host#

sudo ./axlib-1.0.6/install.sh --with-textfsm

Compatibility#

Validation#

Validated on Python 3.14.3 and, from the built wheel, on Python 3.11.16:

Ruff format and lint (all rules, including docs code): passed
ty static type check:                                  passed
Educational docstring audit:                           passed
Main pytest suite:                                     336 passed
Full suite from the built wheel, Python 3.11 and 3.14: 336 passed
Installer in an Ubuntu 26.04 container, from the
  release tarballs: 1.0.5 with the TUI upgraded to
  1.0.6 (options, configuration, and records kept),
  --with-textfsm added, a shared template used,
  before/after diff of the examples, the documented
  rollback to 1.0.5, and the upgrade again:            passed
shellcheck of install.sh:                              passed

1.0.5#

Overview#

This release adds a RADIUS login check. axlib radius asks a RADIUS server whether it accepts a username and password, the same way a switch, router, or firewall does at login. Operators can use it to find out why a device login fails: whether the server rejects the account, the request never reaches the server, or the two sides have different shared secrets. The same check is a Python function, axlib.radius.authenticate(). Nothing else changes.

RADIUS login check#

axlib radius 192.0.2.10 -u "$NETUSER" --password-env NETPASS --secret-file /etc/axlib/radius.secret
Access-Accept from 192.0.2.10 port 1812 for 'alice' in 4.2 ms (attempt 1)
  Reply-Message: Welcome, alice
  Vendor-Specific: vendor=9 type=1 value=shell:priv-lvl=15
  Message-Authenticator: verified

The command is also available as axlib-radius and python -m axlib.radius. docs/RADIUS.md covers the options, the Python API, how the protocol works, and troubleshooting.

Python API#

from axlib.radius import authenticate

result = authenticate("192.0.2.10", "alice", password, secret)  # port=1812 by default
print(result.reply.code_name, result.accepted, result.reply.reply_messages)

A reject is a result, not an exception. RadiusTimeoutError and RadiusError mean that there is no answer that can be trusted. The result never holds the password or the secret. The packet functions hide_password(), build_access_request(), and verify_response() are public, so they can be studied and tested on their own.

Security#

Compatibility#

Validation#

Validated on Python 3.14.3 and, from the built wheel, on Python 3.11.16:

Ruff format and lint (all rules, including docs code): passed
ty static type check:                                  passed
Educational docstring audit:                           passed
Main pytest suite:                                     219 passed
RADIUS tests from the built wheel, Python 3.11 and
  3.14 (RFC 2865 section 7.1 vectors, forged and
  malformed replies, local UDP server):                41 passed
Interoperability with a pyrad server: request,
  hidden password, and Message-Authenticator
  accepted; Accept with Cisco vendor attribute and
  Reject verified; exit statuses 0, 1, and 2 (closed
  port) on both Python versions:                       passed

1.0.4#

Overview#

This release makes a shared host easier to run after the first install. install.sh records its options and reuses them, so upgrades need no options. The axuv helper becomes an installed command, /usr/local/sbin/axuv. The axlib command and ax.getkeys() scripts find the host's configuration without AXLIB_CONFIG_FILE. Every credential record can now carry a short note, which the TUI and the CLIs show and edit. Storage formats are unchanged.

Recorded installer options#

Every run of install.sh writes its options to /etc/axlib/install.env, and the next run starts from them:

sudo ./axlib-1.0.4/install.sh               # an upgrade: no options needed
sudo ./axlib-1.0.4/install.sh --help        # shows the defaults the next run would use

New options turn earlier choices back off: --no-tui, --no-netenv, and --store (the opposite of --no-store). --sbin-dir chooses where axuv is installed. On a host installed by 1.0.3, which wrote no record, the first 1.0.4 run keeps the TUI and the netenv login snippet as they are unless told otherwise.

The axuv command#

/usr/local/sbin/axuv replaces the shell function that administrators added to root's ~/.bashrc. sudo axuv <uv arguments> runs uv on the shared environment with umask 022 and the shared Python, taking the paths from the install record, and it refuses to run as anyone but root:

sudo axuv add netmiko scrapli
sudo axuv lock --upgrade && sudo axuv sync

An existing axuv function in root's ~/.bashrc takes precedence over the command in interactive root shells. It still works, but removing it lets the installed command follow future changes.

Default configuration file#

axlib now selects its TOML file from --config, then AXLIB_CONFIG_FILE, then the CONFIG_FILE in /etc/axlib/install.env. On an installed host, sudo axlib credential-db add ... works without -E or an export, and cron jobs and systemd units need no AXLIB_CONFIG_FILE. axlib.credentials.default_config_file() returns the selected file, and every --config help text states the order. Users who cannot read /etc/axlib see no change.

Record notes#

Every record may now have a note: one line of up to 120 characters for the people who manage the store, such as "NOC team account; owner J. Smith" or "lab account, expires in June".

The note is stored inside the encrypted record like any other field, so neither store's format changes. It is not treated as secret, because listings display it. No lookup helper reads it: ax.getkeys() returns the same values, axlib netenv-set never exports a NOTE variable, and a note in both stores is not reported as overridden. Blank notes, notes over 120 characters, and notes with line breaks or control characters are refused, so a note cannot break a table row or send escape sequences to a terminal.

Fixes#

Compatibility#

Validation#

Validated on Python 3.14.3:

Ruff format and lint (all rules, including docs code): passed
ty static type check:                                  passed
Educational docstring audit:                           passed
Main pytest suite:                                     178 passed
shellcheck install.sh, scripts/axuv:                   passed
TUI notes (add, edit, clear, filter, 80-column note
  line) in headless Textual tests:                     passed
install.sh on Ubuntu 24.04 (fresh, rerun without
  options, --no-tui/--no-netenv, --config-dir change,
  axuv under umask 077, non-root refusal):             passed
Upgrade from a 1.0.3 install with TUI, netenv, and a
  shared library; rollback to 1.0.3 with axuv:         passed
Configuration default as root, a netops operator,
  and a user outside netops:                           passed

1.0.3#

Overview#

This release makes axlib straightforward to deploy on a shared Linux host. The release tarball now contains install.sh, which sets up a shared Python, a locked axlib environment, and a ready credential store for the netops group, and docs/INSTALL.md explains how administrators maintain that environment and how network operations staff use it in their own scripts. The axlib package itself is unchanged from the previous release.

Shared-host installer#

The release tarball now contains install.sh, which installs axlib for every operator on a Linux host:

tar xzf axlib-<version>.tar.gz
sudo ./axlib-<version>/install.sh            # add --with-tui and/or --with-netenv as needed
Path Contents
/opt/shared/python uv-managed Python, readable by every user
/opt/shared/axlib uv project with a locked .venv, release wheels, and docs (current/)
/usr/local/bin/axlib the axlib command
/etc/axlib/axlib.toml, /etc/axlib/sqlite-aes256.key configuration and AES-256 key, root:netops
/var/lib/axlib/credentials.db encrypted store, root:netops 0660

AXLIB_CONFIG_FILE is set for all logins, so operators need no configuration. An existing configuration, key, or store is never overwritten, and rerunning a newer release's installer upgrades in place; previous wheels stay in releases/ for rollback. See docs/INSTALL.md.

Administrator and operator guides#

docs/INSTALL.md now has two guides:

Administrators should use axuv (or an equivalent umask 022) for every change to the shared environment. Running plain uv under a restrictive umask leaves new libraries unreadable for everyone except root, and the damage persists through reinstalls until the uv cache is cleaned. The guide's troubleshooting table gives the symptoms and the recovery steps.

Compatibility#

Validation#

Validated on Python 3.14.3:

Ruff format and lint (all rules, including docs code): passed
ty static type check:                                  passed
Educational docstring audit:                           passed
Main pytest suite:                                     156 passed
shellcheck install.sh:                                 passed
install.sh on Ubuntu 24.04 (fresh, rerun, upgrade,
  rollback, non-root operator, login autoload):        passed
Shared libraries (netmiko, scrapli) as another user,
  kept across an axlib upgrade:                        passed
Personal uv project on the shared Python:              passed

1.0.2#

Overview#

This release lets Network Operations staff load their network login into the shell when they SSH in. axlib netenv-set exports NETUSER, NETPASS, and NETENABLE from the operator's axlib record, and axlib netenv-clear removes them. Storage formats, ax.getkeys(), and the lookup precedence are unchanged.

Session credentials#

A program cannot change its parent shell's environment, so both commands print shell statements for the shell to eval:

eval "$(axlib netenv-set)"      # export NETUSER, NETPASS, NETENABLE
eval "$(axlib netenv-clear)"    # unset them

The commands are also available as axlib-netenv-set, axlib-netenv-clear, and python -m axlib.credentials.netenv set|clear.

uv scripts and shell integration#

Read docs/NETENV.md before enabling this on a shared host. Exported passwords are visible to every program the shell starts, remain in long-running tmux sessions after a password change, and take precedence over the store in ax.getkeys().

Compatibility#

Validation#

Validated on Python 3.14.3:

Ruff format and lint (all rules, including scripts/):  passed
ty static type check:                                  passed
Educational docstring audit:                           passed
Main pytest suite:                                     156 passed
Measured source line coverage:                         82%
Source-distribution build:                             passed
Extracted source-distribution tests:                   156 passed
bash eval round trip, set -x, non-interactive guard:   passed

1.0.1#

Overview#

This release adds a Python administration API for the encrypted credential stores and an optional full-screen credential manager (TUI) for operators who run ax.getkeys() scripts but do not write Python. Storage formats are unchanged: existing SQLite databases, encrypted text files, and key files work without any migration.

Python administration API#

axlib.credentials.StoreAdmin administers either durable store through one API: status(), initialize(), add(), update() (including removal of optional fields), delete(), rotate_key(), and annotate(). It normalizes service names exactly as ax.getkeys() looks them up, validates fields against a record profile, clears the service's Redis cache entry after every change, and never returns a secret value. Both CLIs and the TUI are thin layers over it.

python -m axlib.credentials.admin is a standalone health check (text or --json) that exits 0 only when every enabled store is ready.

Record profiles#

Profiles record which fields each lookup helper expects: network (netuser, netpass, optional netenable) for ax.getkeys() and infoblox (ibgrid, ibuser, ibpass) for ax.getinfoblox(). Infoblox records can now be provisioned from the command line or the TUI. python -m axlib.credentials.profiles lists them.

Command-line changes#

Credential manager (TUI)#

Install the optional extra and start the manager locally or over SSH:

uv add 'axlib[tui]'
axlib credential-tui --config /etc/axlib/axlib.toml

It lists services with notes explaining how ax.getkeys() will use them, and it adds, edits, and deletes records, initializes stores, switches between the SQLite and text-file stores, and rotates keys. Credential values are never displayed. The manager refuses to start when Textual keystroke logging (TEXTUAL_LOG, devtools, or debug mode) is enabled, blocks clipboard copies from password boxes, matches its colors to the terminal, and closes after five idle minutes. See docs/CREDENTIAL_TUI.md.

Version numbering#

The release number now lives only in pyproject.toml and is changed with uv version --bump patch. axlib.__version__ reads it from the installed package metadata, and a test fails if the number is hard-coded anywhere else.

Compatibility#

Validation#

Validated on Python 3.14.3:

Ruff format and lint (all rules):                    passed
ty static type check:                                passed
Educational docstring audit:                         passed
Main pytest suite:                                   137 passed
Measured source line coverage:                       82%
Source-distribution build:                           passed
Extracted source-distribution tests:                 137 passed
TUI in a pseudo-terminal (truecolor/256/16/NO_COLOR): passed

1.0.0#

Overview#

This release establishes the first axlib-owned credential-storage formats and a common administration model for durable credential stores. The former keyring / keyrings.cryptfile backend has been removed. Axlib now provides its own AES-256-GCM encrypted text credential store alongside the existing AES-256-GCM SQLite store.

This is intentionally a breaking release. Credential-store migrations are not implemented: unsupported encrypted-text formats and unsupported SQLite schema versions fail clearly and must be deliberately recreated or reprovisioned by an operator.

Backwards-compatible application lookup#

Existing network automation can continue to use:

import axlib as ax

netuser, netpass, netenable = ax.getkeys()

When no service is explicitly supplied, ax.getkeys() uses $USER for the operator lookup. NETUSER, NETPASS, and NETENABLE continue to override stored fields. If an individual operator is not enlisted or lacks the required login fields, the configured shared service remains the fallback.

The lookup order for missing fields is:

  1. caller/environment values;
  2. Redis, when enabled;
  3. SQLite, when enabled;
  4. the axlib AES-256-GCM encrypted text file, when enabled.

SQLite is therefore the preferred durable store when both durable stores are configured.

Native AES-256-GCM credential file#

The new CredentialFileStore is implemented entirely within axlib. It does not import or depend on keyring or keyrings.cryptfile, does not read legacy cryptfile files, and does not support AES-128.

The format uses:

Text-file writes are protected by a sibling lock file and POSIX advisory locking. Mutations write a complete temporary sibling file, flush it, atomically replace the live credential file, and apply configured ownership/mode policy. This avoids lost updates when multiple Network Operations users administer the same shared store.

Parallel durable-store APIs#

CredentialFileStore and SQLiteCredentialStore use the same operational model:

The text store additionally provides dependency-free convenience methods get_password(), set_password(), and delete_password() for greenfield scripts that prefer the familiar service/field calling style.

Detailed examples for every public method are in docs/CREDENTIAL_FILE.md and docs/SQLITE_CREDENTIALS.md.

Standardized administration commands#

The two durable stores now have parallel command-line interfaces:

axlib credential-file init
axlib credential-file add
axlib credential-file update
axlib credential-file delete
axlib credential-file list

axlib credential-db init
axlib credential-db add
axlib credential-db update
axlib credential-db delete
axlib credential-db list

Standalone entry points are also installed:

axlib-credential-file
axlib-credential-db

Both administration CLIs support the same input patterns where applicable:

--set FIELD=VALUE
--from-env FIELD=ENV_VAR
--prompt FIELD
--dry-run

Listings expose only service/field names and safe timestamps. Credential values are not printed. Successful durable-store modifications invalidate the affected Redis cache entry when Redis caching is enabled.

Configuration changes#

The encrypted text store uses the new [credential_file] configuration section. Historical keyring_file, keyring_key, keyring_key_file, AXLIB_KEYRING_*, and axlib-set-keys interfaces have been removed.

Shared-host example:

[credentials]
shared_service = "network-shared"

[credential_file]
enabled = true
file = "/var/lib/axlib/credentials.axc"
key_file = "/etc/axlib/credential-file.key"
lock_timeout = 5.0
file_mode = "0660"
key_file_mode = "0640"
group = "netops"
enforce_permissions = true

[sqlite]
enabled = true
database = "/var/lib/axlib/credentials.db"
key_file = "/etc/axlib/sqlite-aes256.key"
timeout = 5.0
database_mode = "0660"
key_file_mode = "0640"
group = "netops"
enforce_permissions = true

For both stores, relative data/key paths in TOML are resolved relative to the selected configuration file. Key files may live independently of the database/credential-file path.

Direct key injection is also supported through the environment; direct encryption keys are not read from TOML:

AXLIB_CREDENTIAL_FILE_KEY
AXLIB_SQLITE_KEY

The corresponding key-file variables are:

AXLIB_CREDENTIAL_FILE_KEY_FILE
AXLIB_SQLITE_KEY_FILE

Shared and private permission models#

The shared-server defaults are intended for operators in the netops group:

Private deployments can select 0600 modes and disable group enforcement by configuring an empty group. Existing symlinks and non-regular sensitive paths are rejected.

Storage-version policy: no migrations#

This release deliberately does not contain credential-store migration code.

For the encrypted text store, initialize() creates the current format when the file is absent and verifies the exact supported format/version/cipher when it exists. Legacy cryptfile data, AES-128 data, unknown versions, and alternate formats are rejected.

For SQLite, initialize() creates the current schema for a new database and verifies the exact current schema version for an existing database. Older or newer schema versions are rejected rather than upgraded or rewritten.

This policy keeps the initial production formats and educational code straightforward. Migration logic can be added later only if an operator explicitly requires it.

Dependency changes#

Runtime dependencies are now:

cryptography>=42
redis>=5

Removed dependencies:

keyring
keyrings.cryptfile

There is no optional keyring adapter and no keyring entry-point registration.

Validation#

The final release was validated on Python 3.13.5:

Source compilation:                         passed
Educational docstring audit:                passed
Main pytest suite:                          78 passed
Measured source line coverage:              76%
Wheel build:                                passed
Source-distribution build:                  passed
Wheel metadata/dependency audit:            passed
Wheel entry-point audit:                    passed
Clean installed-wheel credential-file use: passed
Installed ax.getkeys() lookup:              passed
Installed shared-service fallback:          passed
Text-store convenience password methods:    passed
Extracted source-distribution tests:         78 passed
Unsupported text format/version rejection:  passed by test suite
Unsupported SQLite schema rejection:        passed by test suite

A live Redis service was not required for release validation. Redis behavior, precedence, and invalidation boundaries are exercised through the test suite without placing production secrets in test output.