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"
- What it reads: Markdown as GitHub reads it: tables, task-list check boxes, footnotes, strikethrough, and bare URLs as links. Headings get GitHub's anchors, so
INSTALL.md#offline-hostslinks keep working. - The pages: a sidebar down the left lists every page converted together and the current page's sections, and an optional bar at the top holds the site name and links such as the project's repository (
--site-name,--link LABEL=URL). Pages need no JavaScript, web server, or internet access, follow the light or dark system setting, and put the text first on a phone. - Links: links between the Markdown files are rewritten to the HTML pages. Broken links, missing files, and
#sectionlinks to headings that do not exist are reported asfile:line: message, and--strictmakes them fail the command. Linked images are copied next to the pages; hidden files are never copied. - Python:
markdown_to_html()for one string,convert_paths()for files and folders. - Install:
uv add 'axlib[markdown]', orinstall.sh --with-markdownon a shared host (under 1 MB of pure Python).
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#
- Credentials: a key file or database named by a relative path in
axlib.tomlthat is a symlink is now refused, like any other symlink.axlib credentialsreports a missing configuration in one line instead of a traceback, and a configuration file a user cannot read is reported ascannot be read (Permission denied)rather thandoes not exist. SQLite key-file problems are reported as themselves.rotate-key --dry-runand the TUI make the same checks as the rotation itself. The TUI empties password boxes whenever a form closes. install.sh:--no-storeno longer turns the SQLite store on in a newaxlib.toml.currentmoves to a new release only after it is installed, and a failed install restores the project and removes the unfinished release. A failing store check at the end is a warning, not a failed install. EveryAXLIB_*variable in root's environment is ignored while it runs.axlib parse: commands typed in a configuration mode withdoare parsed.axlib parse diffexits 2 when a command failed to parse, and a damaged snapshot is an error instead of an empty capture.axlib radius: end of input at a prompt is explained, and--password-stdinwith no input no longer sends an empty password.- Text filters:
triple-quoteshandles CRLF and CR-only text;includekeeps an included file without a final newline on its own line and blocks a cycle back to the input file;includeandcommentsreport undecodable input with status 1;variableskeeps the last of repeated assignments.
Compatibility#
axlib netenv-setoutput: for a record without an enable secret, it no longer printsunset NETENABLE; its output is onlyexportlines.--unsetbrings the line back. Thenetenv-setshell function inexamples/netenv/netenv.sh, installed as/etc/profile.d/axlib-netenv.shby--with-netenv, always passes--unset, so login shells behave as before. A script that runseval "$(axlib netenv-set)"itself keeps an existingNETENABLEunless it adds--unset.- Exit statuses:
axlib parse diffexits 2, not 0 or 1, when a command failed to parse.axlib credentialsexits 2 when it cannot run at all, keeping 1 for a missing field.includeandcommentsexit 1, not 2, for undecodable input. import axlib.secretsno longer reads the configuration file when it is imported; settings are read at the first lookup.- No storage format changes. A shared host upgrades in place with
sudo ./axlib-1.0.7/install.sh, keeping its options; add--with-markdownfor the converter. Rolling back to 1.0.6 asdocs/INSTALL.mddescribes works; uv warns that 1.0.6 has nomarkdownextra.
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.
- Captures can be session logs with IOS, NX-OS, EOS, IOS-XR, or Junos prompts, script output with bare
showordisplaycommands, files and directory trees (-r), output piped fromssh(--command), or another header format described with--marker. Pager prompts, IOS timestamp lines, and Junos{master:0}lines are removed first.axlib parse splitshows how a capture is cut into commands. - Abbreviations such as
sh cdp neiare recognized, and they pair withshow cdp neighborsin the other collection. - Records are made comparable. They are sorted by what identifies them, such as the local interface and neighbor. Interface names are spelled one way (
Gig 1/0/48,Gi1/0/48→GigabitEthernet1/0/48). Fields that change on every run, such as hold time, are dropped. A template'sKeyfields identify its records; built-in profiles cover CDP and LLDP neighbors (brief and detail), IP interface brief, and interface status. - Output is a JSON snapshot by default, which
axlib parse diffreloads, or JSON lines, or tables.axlib parse diff --jsonprints the comparison as one object. - Exit status for
axlib parse: 0 parsed, 1 a command failed or nothing was parsed, 2 error. Foraxlib parse diff, as fordiff: 0 no differences, 1 differences, 2 trouble.
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:
--template-dir DIR(repeatable)AXLIB_TEXTFSM_TEMPLATEStemplate_dirsin the new[textfsm]table ofaxlib.toml, ortextfsm/besideaxlib.tomlwhen it has none. On a shared host this is/etc/axlib/textfsm.- 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
--with-textfsminstalls the extra and creates/etc/axlib/textfsm, owned byroot:netopswith mode2775, so any member ofnetopscan add templates. Runchmod 2755 /etc/axlib/textfsmto allow only root; later runs keep the mode. The choice is recorded asWITH_TEXTFSMin/etc/axlib/install.env, so upgrades keep it.--no-textfsmremoves the packages and keeps the templates.- Removing one optional part now keeps the other. Before, the installer recognized only the credential manager installed on its own.
- A new
axlib.tomlincludes a commented[textfsm]table. An existing one is not changed and needs no edit:/etc/axlib/textfsmis found because it is beside it.
Compatibility#
- New package and commands only:
axlib.parse,axlib parse, andaxlib-parse. No existing command, API, storage format, orax.getkeys()behavior changes. - No new required dependencies. The optional
textfsmextra addstextfsmandntc-templates, about 5 MB of pure Python, both Apache-2.0. Without it,axlib parseexplains how to install it and exits 2. Splitting captures and comparing two saved snapshots need only the standard library. - Upgrading a shared host with
sudo ./axlib-1.0.6/install.shkeeps its options and does not add parsing; add--with-textfsmfor that. - Rolling back to 1.0.5 as described in
docs/INSTALL.mdworks. uv warns that the 1.0.5 wheel has notextfsmextra, and the parsing packages stay installed but unused.
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
- It sends one PAP
Access-Requestto UDP port 1812, or the port given with--port. After--timeoutseconds it resends the identical packet, up to--retriestimes. - It prints the answer with the reply's attributes, or one JSON object with
--json. The exit status is 0 for Access-Accept, 1 for Access-Reject or Access-Challenge, 2 for an error, and 3 when no reply arrives, so scripts and monitoring checks need no parsing. - The password comes from an environment variable (
--password-env NETPASSworks afternetenv-set), from standard input, or from an echo-off prompt. The shared secret comes from a file, an environment variable, or a prompt.--passwordand--secretalso work, but the help text warns that other users can see them inps. - Errors explain the likely cause. If no reply arrives, the message lists a blocked port, this host not being configured as a client, and a wrong secret that the server drops silently. A reply that fails verification is reported as a probable shared-secret mismatch, and a closed port as "nothing is listening".
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#
- Every request carries a
Message-Authenticator, the defense against the BlastRADIUS reply-forgery attack (CVE-2024-3596). Servers configured to require it accept these requests. - Every reply's Response Authenticator is verified, and so is its Message-Authenticator when it has one. The output reports whether the server sent one, because a server that never does may still need the BlastRADIUS update.
- PAP hides the password only with MD5 and the shared secret. Run checks over a trusted management network.
Compatibility#
- New module and commands only:
axlib.radius,axlib radius, andaxlib-radius. No existing command, API, storage format, orax.getkeys()behavior changes. - No new dependencies. The module uses only the Python standard library, and its MD5 calls are marked
usedforsecurity=Falseso they also work under FIPS-mode Python. - Only PAP is supported. CHAP, MS-CHAP, EAP, accounting, and RADIUS over TLS are not, and an Access-Challenge, such as an OTP prompt, is reported but not answered.
- A shared host upgrades in place with
sudo ./axlib-1.0.5/install.sh. The installer needs no new options.
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".
- In the TUI, the Add and Edit forms have a Note box, and clearing it removes the note. The table has a NOTE column showing the first 40 characters. The line below the table shows the whole note of the highlighted record, so it is readable on an 80-column terminal. The
/filter matches note text as well as service names. The column that explains howax.getkeys()treats each record (you,shared fallback,missing netpass) is renamed from NOTES to ADVICE. - In the CLIs,
--set note="..."and--remove notework withaddandupdate.listadds a NOTE column as the last column, andlist --jsonadds"note"(nullwithout one). - In Python,
StoreAdmin.list_records()returns each note inrecord.visible["note"], andAnnotatedRecord.notegives it directly.
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#
--no-tuiremoves the credential manager's packages from the shared environment. In 1.0.3, leaving out--with-tuiwas documented to remove the TUI but did not, becauseuv addkeeps an existing extra and never uninstalls packages.- Rerunning the installer with a different
--config-dirnow replacesAXLIB_CONFIG_FILEin/etc/environmentinstead of keeping the old path.
Compatibility#
- On a host with
/etc/axlib/install.env, a program that previously ran without any configuration file now reads the recordedaxlib.toml. SetAXLIB_CONFIG_FILEor pass--configto choose a different file. - A 1.0.3 host upgrades in place with
sudo ./axlib-1.0.4/install.sh. Its configuration, key, store, records, and added libraries are kept. - Mirror settings for offline hosts belong in
/etc/uv/uv.toml, which every uv run reads, instead of the oldaxuvfunction. listoutput has a fifth column, NOTE, at the end, andlist --jsonobjects have anotekey. Scripts that read the first four columns by position, or ignore unknown JSON keys, are unaffected. The TUI's advice column is now titled ADVICE.- A record with a note stays usable by 1.0.3: lookups ignore the field, and the 1.0.3 TUI and
listshownoteas an extra field name. The 1.0.3 CLIs refuse--set note=.... - No storage format, dependency, or
ax.getkeys()precedence changes.
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:
- For administrators: an
axuvhelper that runs uv on/opt/shared/axlibwith the shared Python andumask 022; adding libraries such as netmiko and scrapli so every script can use them; pinning, upgrading, and removing libraries; trackingpyproject.tomlanduv.lockin git for one-command undo; upgrades, rollback, backups, mirrors, and uninstalling. Libraries added this way survive axlib upgrades. - For network operations staff: four ways to get credentials in a script: the shared interpreter (
#!/opt/shared/axlib/.venv/bin/python, with netmiko and scrapli examples), a personal uv project or single-file uv script using the release wheel from/opt/shared/axlib/releases/, or theNET*variables fromnetenv-set. It also covers cron and systemd (useax.getkeys(getpass.getuser())whereUSERmay be unset) and rules for handling credentials.
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#
- The axlib package is unchanged from the previous release: no API, command, storage format, or dependency changes.
- Hosts that already have axlib installed can adopt the installer by running it. An existing configuration, key, or store is kept and only verified.
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
$USERselects the record, as inax.getkeys().--service NAMEloads another record.- Existing
NET*variables are ignored during the lookup, so a stale value never looks like a successful load. - The shared fallback service is used only with
--allow-shared, and always with a warning. - A record without an enable secret unsets
NETENABLE. - Values are quoted with
shlex.quote; standard output carries only shell code, and every message goes to standard error. - On any failure nothing is printed on standard output and the environment is unchanged. Exit statuses:
0exported,1no complete record,2configuration or store error,3refused because standard output is a terminal (which keeps the password off the screen). --checkreports which fields would be set without showing values.
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#
scripts/netenv-setandscripts/netenv-clearare uv inline-metadata scripts (#!/usr/bin/env -S uv run --quiet --script) that install axlib from the checkout they live in and run from any directory. Call them by their real path; a symlink moves the directory uv resolves the checkout from.examples/netenv/netenv.shdefinesnetenv-setandnetenv-clearshell functions for bash and zsh and loads credentials when an interactive shell starts. Non-interactive sessions (ssh host command,scp,sftp,rsync) are left alone,set -xtracing is paused during theeval, and the functions return the command's real exit status. Install it in~/.bashrc,~/.zshrc, or/etc/profile.d/.
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#
- No storage format or schema changes, and no migrations.
- No new runtime dependencies.
ax.getkeys(),ax.getinfoblox(), and existing commands are unchanged.
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#
addandupdateaccept--profile network|infoblox(defaultnetwork).update --remove FIELDdeletes an optional field; required fields cannot be removed.rotate-keyre-encrypts every record under a new AES-256 key (--generate-keyor--new-key-file, with--dry-runand a required--yes).- When a change is saved but the Redis cache cannot be cleared, the command now prints
status=...andcache=stalebefore exiting1, so it no longer looks like a failed save. axlib credential-dbandaxlib credential-filenow share one implementation; existing command lines and output are unchanged.
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#
- No storage format or schema changes, and no migrations.
ax.getkeys(),ax.getinfoblox(), and the lookup precedence are unchanged.- Runtime dependencies are unchanged (
cryptography,redis). The optionaltuiextra addstextual>=8.2.8andrich>=15.0.0. normalize_service_for_writemoved toaxlib.credentials.managerand remains importable fromaxlib.credentials.cli_common.RELEASE_NOTES.mdis now included in the source distribution.
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:
- caller/environment values;
- Redis, when enabled;
- SQLite, when enabled;
- 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:
- AES-256-GCM from
cryptography; - a 32-byte URL-safe Base64 encryption key;
- a fresh 12-byte nonce for each encrypted service write;
- authenticated associated data that binds ciphertext to the normalized service identity;
- an encrypted key-check value so an incorrect key is rejected before normal credential use;
- an INI-like outer container with version/cipher metadata and encrypted service payloads;
- JSON serialization inside each encrypted service payload so both field names and field values are encrypted.
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:
initialize()— create a new current-format store, or verify an existing current-format store;create()— create a service and fail if it already exists;read()— return requested decrypted fields;update()— modify fields and fail if the service does not exist;write()— atomically create a service when absent or merge fields when present;delete()— remove selected fields;delete_service()— remove the complete service record;list_records()— return service names, field names, and safe metadata without exposing secret values.
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:
- encrypted credential file:
0660; - credential-file lock file:
0660; - SQLite database:
0660; - encryption key files:
0640; - configured group:
netops.
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.