UniFI CLi and declarative engine
  • Rust 98%
  • Shell 2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Claude f160869222 phase 3: close issues #1-#7
- #1 auto-retry on HTTP 429: RetryPolicy threaded through Ctx;
  paginate::fetch_all wraps each page request with retry::call.
  CLI flags --max-retries (default 3) and --retry-cap-secs (default 60).
- #2 Retry-After HTTP-date parsing: parse_retry_after now accepts RFC
  2822 / RFC 850 / asctime forms in addition to integer seconds.
- #3 structured error body in ApiError.details: added Option<Value>
  details to Http/Unauthorized/BadRequest/Server variants; populated
  by from_status_with via parse_details (JSON or string-wrapped);
  surfaced in the JSON envelope.
- #4 structured --filter JSON: util::read_filter and read_filter_expr
  accept @path / - / raw forms uniformly; applied to every paginated
  list arm; device-tag uses the FilterExpression form.
- #5 opinionated per-resource human tables: output::tables registry
  matches known resource shapes by signature fields and projects
  curated column lists; list envelopes unwrap to data rows + total
  count; nested values rendered as <obj>/[N] in cells.
- #6 e2e smoke: scripts/smoke.sh runs read-only sweep against a real
  controller (UNIFI_BASE_URL/UNIFI_API_KEY/UTILIFI_SITE env), prints
  PASS/FAIL per group, no mutations.
- #7 README beyond Phase 0: rewrote front matter, added command-group
  table, global flags, pagination/filter/body conventions, error
  details semantics, smoke runner usage.
2026-05-11 14:35:33 -07:00
.omc phase 2.3: mock coverage for all 73 ops + pagination + retry 2026-05-11 13:31:26 -07:00
crates phase 3: close issues #1-#7 2026-05-11 14:35:33 -07:00
lib first. 2026-05-11 01:43:01 -05:00
scripts phase 3: close issues #1-#7 2026-05-11 14:35:33 -07:00
spec phase 0.3: strip type-mismatched enum decorations; real codegen succeeds 2026-05-11 11:51:15 -07:00
.gitignore readding omc for context 2026-05-11 02:00:39 -05:00
Cargo.lock phase 2.2: mock test harness + sites coverage 2026-05-11 13:24:31 -07:00
Cargo.toml phase 0.3: strip type-mismatched enum decorations; real codegen succeeds 2026-05-11 11:51:15 -07:00
FRONTMATTER.xml phase 0.1: normalize openapi schema names for codegen 2026-05-11 01:52:41 -05:00
README.md phase 3: close issues #1-#7 2026-05-11 14:35:33 -07:00
rust-toolchain.toml first. 2026-05-11 01:43:01 -05:00
rustfmt.toml first. 2026-05-11 01:43:01 -05:00

utilifi

A Rust CLI (utilifi) wrapping the UniFi Network Integration API.

The crate ships a complete set of subcommands covering every operation in the controller's OpenAPI spec across 12 resource groups: sites, devices, clients, networks, wifi, hotspot, firewall, acl, switching, dns, traffic, supporting. All paginated reads support --all to auto-follow pages, and HTTP 429 responses are auto-retried using the controller-supplied Retry-After value.

Layout

  • crates/unifi-api — generated OpenAPI client (progenitor build.rs)
  • crates/unifi-cli — the utilifi binary (clap, tokio)
  • crates/xtask — workspace task runner; owns spec normalization
  • spec/ — OpenAPI source of truth, down-converted 3.0.3, normalized variant
  • lib/ — archival snapshots (do not modify)
  • scripts/ — spec tooling and the smoke.sh end-to-end script

Build

cargo build --workspace

Run:

./target/debug/utilifi --help
./target/debug/utilifi --version

Quickstart

export UNIFI_API_KEY=...
utilifi --base-url https://192.168.1.1 --insecure sites list
utilifi --base-url https://192.168.1.1 --insecure --site <UUID> devices list --all

Command groups

Every group has the usual list/get/create/update/delete shape where applicable.

Group Sample actions
sites list, info
devices list, get, stats, pending, tags, adopt, action
clients list, get, disconnect, block, unblock, authorize
networks list, get, create, update, delete, plus VPN ops
wifi list, get, create, update, delete, radius-*
hotspot vouchers list/create/delete, guests *
firewall zones *, policies *
acl list, get, create, update, delete
switching lags, mclags, stacks
dns records *, policies *
traffic lists *
supporting countries, dpi apps/categories

Run utilifi <group> --help and utilifi <group> <action> --help for the authoritative argument list per command.

Global flags

  • --base-url URL — controller API base; overrides config
  • --site UUID — site to operate on; overrides config
  • --insecure — accept self-signed TLS (UniFi default)
  • --output {human,json} — output format (default human)
  • --config PATH — alternative config file
  • -v, -vv, -vvv — increase logging verbosity
  • --max-retries N — retries on HTTP 429 (default 3)
  • --retry-cap-secs N — upper bound (seconds) on a single Retry-After sleep (default 60)

Pagination

Every list-style action accepts --limit, --offset, and --all. With --all, the CLI walks pages internally and emits a single concatenated envelope. Each page request is wrapped in the 429 retry policy described above; a Retry-After value (integer seconds or HTTP-date) drives the sleep.

--filter and --body

Both flags accept three forms uniformly:

  • raw inline JSON / filter expression
  • @path — read from a file
  • - — read from stdin

For endpoints that take a structured FilterExpression (e.g. devices tags), valid JSON is passed through as-is and a non-JSON string is wrapped as a JSON string.

Credentials

Resolved in this order by unifi-cli:

  1. UNIFI_API_KEY environment variable
  2. [network] api_key = "..." in the TOML config file at $XDG_CONFIG_HOME/utilifi/config.toml (typically ~/.config/utilifi/config.toml)
  3. OS keyring entry service="utilifi" user="network-api-key"

If none are set, the CLI exits with a clear MissingCredentials error.

Output

  • --output human (default): tables on stdout, status/log on stderr.
  • --output json: single-line envelope on stdout: {"ok": bool, "data": <T> | null, "error": {"code","message","details"} | null}.

When the controller returns a non-2xx response, the error.details field is populated with the controller-supplied body (parsed as JSON when possible, wrapped as a string otherwise).

Exit codes

  • 0 success
  • 2 user error (bad args, missing credentials, missing site)
  • 3 API error (controller returned a non-2xx response)
  • 4 network/transport error

End-to-end smoke

scripts/smoke.sh runs a read-only sweep against a real controller. Required env: UNIFI_BASE_URL, UNIFI_API_KEY. Optional: UTILIFI_SITE, UTILIFI_INSECURE=1. No mutations are issued, so it is safe to run against production hardware.

UNIFI_BASE_URL=https://192.168.1.1 \
UNIFI_API_KEY=... \
UTILIFI_INSECURE=1 \
./scripts/smoke.sh

Codegen pipeline

  1. The live spec lives at spec/unifi-network-api-10.3.58.json (OpenAPI 3.1.0, fetched from the controller).
  2. scripts/down-convert-spec.sh produces spec/unifi-network-api-10.3.58.openapi-3.0.json because progenitor is based on openapiv3 (3.0-only).
  3. cargo run -p xtask -- normalize-spec rewrites every component schema name that is not a valid Rust identifier (the live UniFi spec uses display-style names with spaces, e.g. "ACL rule") and rewrites every $ref and discriminator mapping in lockstep, producing spec/unifi-network-api-10.3.58.normalized.json plus a sorted name-map.json for review. build.rs invokes this automatically.
  4. crates/unifi-api/build.rs runs progenitor over the normalized file at build time and writes the result into $OUT_DIR/codegen.rs. On the current spec this succeeds end-to-end; spec/codegen-failures.log reads codegen: ok.

To debug a future codegen regression, run

cargo run -p xtask -- bisect-codegen

which narrows components.schemas down to the minimum set of schemas that still reproduce a typify failure and writes spec/codegen-bisect.json. See spec/README.md for the full pipeline.

To regenerate the down-converted spec:

./scripts/down-convert-spec.sh
cargo run -p xtask -- normalize-spec

Conventions

See FRONTMATTER.xml and the global rules under ~/.claude/CLAUDE.md: 2-space indent, ASCII-only, files under 500 LOC, functions under 50 LOC, inline #[cfg(test)] mod tests.