- Rust 98%
- Shell 2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
- #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. |
||
| .omc | ||
| crates | ||
| lib | ||
| scripts | ||
| spec | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| FRONTMATTER.xml | ||
| README.md | ||
| rust-toolchain.toml | ||
| rustfmt.toml | ||
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 (progenitorbuild.rs)crates/unifi-cli— theutilifibinary (clap, tokio)crates/xtask— workspace task runner; owns spec normalizationspec/— OpenAPI source of truth, down-converted 3.0.3, normalized variantlib/— archival snapshots (do not modify)scripts/— spec tooling and thesmoke.shend-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 (defaulthuman)--config PATH— alternative config file-v,-vv,-vvv— increase logging verbosity--max-retries N— retries on HTTP 429 (default3)--retry-cap-secs N— upper bound (seconds) on a singleRetry-Aftersleep (default60)
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:
UNIFI_API_KEYenvironment variable[network] api_key = "..."in the TOML config file at$XDG_CONFIG_HOME/utilifi/config.toml(typically~/.config/utilifi/config.toml)- 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
0success2user error (bad args, missing credentials, missing site)3API error (controller returned a non-2xx response)4network/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
- The live spec lives at
spec/unifi-network-api-10.3.58.json(OpenAPI 3.1.0, fetched from the controller). scripts/down-convert-spec.shproducesspec/unifi-network-api-10.3.58.openapi-3.0.jsonbecauseprogenitoris based onopenapiv3(3.0-only).cargo run -p xtask -- normalize-specrewrites 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$refand discriminator mapping in lockstep, producingspec/unifi-network-api-10.3.58.normalized.jsonplus a sortedname-map.jsonfor review.build.rsinvokes this automatically.crates/unifi-api/build.rsrunsprogenitorover 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.logreadscodegen: 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.