Architecture¶
Module layout¶
pyproject.toml metadata, runtime deps, `ibcli` console-script entry point
src/ibcli/
├── __init__.py version string
├── __main__.py python -m ibcli
├── cli.py argparse, startup, REPL loop, batch/exec modes
├── registry.py COMMANDS dict, @command / register() decorators, SPECOPS, ALIASES
├── parser.py expand_line, expand_word, abbrev, get_context
├── completer.py prompt_toolkit IbcliCompleter (async-aware)
├── dispatcher.py process_line: alias sub + expand_line + dispatch + error unwrap
├── context.py Context dataclass (client, prompt, online, host, caches, …)
├── completions.py Dynamic completions (members, zones, views) sourced live from the grid
├── debug.py debug_cli level-gated logging
├── utils.py cidr_family, normalize_cidr_any, net_to_arpa, as_dict
└── commands/ One module per functional domain - see README for the full list
├── __init__.py imports every module so @command / register() decorators fire
├── system.py help / quit / history / show time / show debug
├── server.py configure server / master (connect)
├── zone.py authoritative zones + classic records (A/AAAA/CNAME/MX/TXT/PTR/HOST)
├── zone_modern.py Phase-3 records (CAA, DNAME, ALIAS, NAPTR, TLSA, HTTPS, SVCB, shared)
├── dns.py DDNS principal clusters, DNS64, RPZ order, all-records aggregator
├── network.py networks (v4+v6), containers, shared networks
├── ipam.py address inventory, network views, VLAN, superhosts, bulkhosts
├── dhcp.py ranges, fixed, MAC filters, IPv6 DHCP, all 5 filter types
├── grid.py members, licensing, DNS/DHCP enable, HA, restart
├── grid_ext.py grid DNS/DHCP properties, threat feeds, file distribution, GMC
├── admin.py admin users/groups/roles, RADIUS legacy objects
├── auth.py auth services (LDAP, AD, RADIUS, TACACS+, SAML, cert, policy)
├── auth_ext.py approval workflow, FTP/SNMP users, parental control, HSM
├── cert.py member certs, CSR, self-signed, upload/download
├── dtc.py DTC servers/pools/LBDNs/monitors/topology/records
├── rpz.py RPZ zones + records (A, CNAME, TXT, IP, client-IP, …)
├── notify.py notification endpoints + rules + templates; ACLs
├── ops.py DB snapshot, CSV tasks, capacity, integrations, BFD, TFTP
├── fileops.py upload/download - CSV, backups, support bundles, per-member dumps
├── discovery.py network discovery, SDN networks, vDiscovery
├── ea.py Extensible Attribute definitions
├── ms.py Microsoft DNS/DHCP integration
├── threat.py Threat Insight, ADP, analytics
├── upgrade.py upgrade groups, schedules, status
└── template.py network and range templates
Data flow¶
User input
│
▼
cli.py : _run_repl / _run_batch / -e
│
▼
dispatcher.py : process_line(line, ctx)
├── apply ALIASES prefix substitution
├── parser.expand_line(line) → (expanded, error, match_line)
│ ├── tokenize (handles quotes, splits key=value)
│ └── per-token: get_context → expand_word → update match_line
├── if error → print error, return
├── if entry is an intermediate waypoint → print "Incomplete : …"
├── if mutating verb → tee stdout into a buffer so we can synthesise "OK"
│ when the handler finishes silently
└── await COMMANDS[match_line].func(expanded, ctx)
│
▼
handler in commands/*.py (async)
│
├── extract args from expanded line (regex + _parse_kv helpers)
├── await ctx.client.<resource>.list / create / update / delete
│ │
│ ▼
│ ibx_nios_sdk.NiosClient → httpx.AsyncClient
│ (cookie auth, async pagination, streaming fileop)
└── print output
When a handler raises NiosError, the dispatcher unwraps nested
AdmConDataError(...) / IBDataConflictError(...) envelopes and prints
either Error: <clean msg> or, in idempotent mode on a conflict,
Skipped: <clean msg>.
Design decisions¶
Decorator-driven registration¶
Commands register themselves via @command(match_line, words=..., help=...) at import time. No external config files,
no discovery phase. src/ibcli/commands/__init__.py imports every command module so all decorators fire before
main() runs.
Adding a new command domain:
- Drop a new file in
src/ibcli/commands/. - Add an import to
commands/__init__.py. - Write
@command(...)handlers. Tab completion and help metadata pick up automatically.
Flat COMMANDS dict¶
The command tree is stored as a flat dict keyed by canonical match-line strings:
COMMANDS["configure zone add <zone>"] = CommandEntry(func=cli_add_zone, words="…", help="…")
COMMANDS["configure zone"] = CommandEntry(words="add <zone>")
Intermediate waypoints (no handler) use register(match_line, words=..., help=...). Depth is encoded in the key string; no tree structure to walk. Lookups are O(1).
Async end-to-end¶
Every handler is async def. NiosClient is built on httpx.AsyncClient; list/create/update/delete calls are
awaited. prompt_toolkit integrates via its asyncio event loop, and cli._run_repl runs inside asyncio.run.
This matches the shape of modern NIOS use cases (lots of I/O, occasional parallelism via asyncio.gather) without forcing the REPL into a thread.
Context object¶
A single Context dataclass flows through the session. Key fields:
| Field | Purpose |
|---|---|
client: NiosClient \| None |
None until configure server succeeds |
host / user |
Current connection's identity |
client_rev |
Negotiated WAPI version (used as default for version-gated calls) |
prompt |
Displayed by the REPL; updates on connect/disconnect |
debug_level |
Toggled via -d <0-5> or the debug command |
online |
Connection flag the REPL checks before issuing WAPI calls |
idempotent |
When set by -i, the dispatcher demotes conflicts to Skipped |
caches |
Sync-accessible completion caches (members, zones, views) populated lazily off the event loop |
Handlers mutate ctx directly (e.g. ctx.client = NiosClient(...) on connect).
WAPI via ibx-nios-sdk¶
The HTTP layer is not in-tree. It lives in the Infoblox-published ibx-nios-sdk, which:
- Detects the WAPI version via
GET /wapi/v2.14/?_schema=1. - Persists the
ibapauthcookie returned on login. - Provides typed resources under
ctx.client.<domain>.<object>(e.g.ctx.client.dns.record_a,ctx.client.ipam.network). - Handles pagination, fileop multi-step protocols, and error unwrapping.
ibcli stays thin: it constructs WAPI bodies, awaits the SDK's CRUD calls, and formats results.
Tab completion via prompt_toolkit¶
IbcliCompleter subclasses prompt_toolkit.completion.Completer. On every <Tab>:
- Parse the line so far via
expand_line. - If parsing errored with "Unknown", suppress completions.
- Use
lastargto identify the current partial word. - Call
get_context(match_line)for the valid next-tokens. - Call
expand_word(lastarg, words)to filter + rank. - Yield
Completionobjects. Each entry'sdisplay_metapullshelp=from the registry, giving the tooltip you see in the dropdown.
Complete-while-typing is disabled (complete_while_typing=False) - completions fire only on <Tab>, matching the Perl UX.
Dynamic completions (member FQDNs, zone names, view names) come from the ctx.caches dict, populated asynchronously on connect so the sync completer has data to surface without awaiting.
Key design constraints¶
- Async end-to-end. Every handler is
async def; the REPL runs inasyncio.run. - No plugin system. Commands register at import time only.
- Python 3.11+. Uses
str | Noneunion syntax,asyncio.Runner, and other 3.11-era features. - Single
Contextper process. No multi-grid state; switching grids means callingconfigure serveragain. - Flat registry. No tree traversal; match-lines are the source of truth for grammar.
- Thin over the SDK. ibcli builds bodies and prints output; it does not reimplement WAPI semantics.