ubitofu turns a live UniFi Network controller into reviewable HCL for the
ubiquiti-community/unifi provider. An administrator can make changes in the
UniFi web UI or mobile app, while an infrastructure repository can make other
changes in HCL. ubitofu captures independent controller changes and blocks when
the controller and HCL changed the same comparable value differently.
The tool is deliberately plan-only. It reads the controller, OpenTofu plans,
and the local module, then writes HCL and structured results. It never writes
the controller, runs tofu apply, changes state, manages Git, or owns CI and
deployment policy.
pip install ubitofuubitofu 0.10 supports CPython 3.11 through 3.14 on macOS and Linux. Windows is unsupported. The tested OpenTofu line is 1.12.x.
Both entry points expose the same commands:
ubitofu --help
python -m ubitofu --helpEvery command takes --config. A UniFi OS controller using an environment
variable for its API key can use:
controller_url = "https://controller.example.test"
site = "default"
api_key_source = "env"
api_key_ref = "UNIFI_API_KEY"
workdir = "./network"TLS certificate verification is enabled by default. To trust a private CA, keep verification enabled and name its bundle:
verify_tls = true
ca_bundle = "/etc/ssl/private-controller-ca.pem"Disabling verification is an explicit local configuration choice. A custom CA cannot be combined with disabled verification.
Self-hosted classic controllers use cookie login:
controller_url = "https://controller.example.test"
site = "default"
workdir = "./network"
dialect = "classic"
username = "admin"
password_source = "env"
password_ref = "UNIFI_PASSWORD"Credential sources may be env or op. With op, set op_vault and use a
1Password reference for the corresponding *_ref setting.
Version 0.10 exposes seven operations:
ubitofu generate
ubitofu reconcile
ubitofu reconcile --dry-run
ubitofu check --plan PLAN
ubitofu inspect
ubitofu health snapshot
ubitofu health compare --before RECEIPT
Pass --config CONFIG to each operation. --format human|json selects output,
and --output PATH writes it to a private file instead of standard output.
Configure the object in the UniFi UI or mobile app first, initialize the OpenTofu root, then run:
ubitofu generate --config config.tomlGeneration asks OpenTofu for provider-shaped configuration and commits one
validated candidate set. It writes ubitofu-owned resources, import blocks,
sensitive variable declarations, and COVERAGE.md. Existing operator content
or ambiguous ownership blocks the whole operation rather than being replaced.
Do not run another OpenTofu process, Git automation, or a file watcher against the same root while generation is active. OpenTofu requires a short-lived import scaffold in the root during initial generation. ubitofu removes the exact scaffold before it interprets plan output and recovers only residue it can identify exactly.
Preview controller changes without writing HCL:
ubitofu reconcile --dry-run --config config.toml--dry-run is a first-class safety interface. It collects the same snapshot
and computes the same decisions, candidate bytes, changed paths, and candidate
digests as a wet reconcile given equivalent inputs. It does not start an HCL
transaction, clean old transaction residue, or normalize existing recovery
metadata before reporting that residue.
Review the decisions and proposed paths, then run the wet command against the same intended inputs:
ubitofu reconcile --config config.tomlThe planner compares the last managed value, evaluated HCL intent, and the live controller value. Its rules are:
- a controller-only change is captured in HCL
- an HCL-only change is preserved
- identical changes on both sides are treated as converged
- different changes to the same comparable value block every write
- independent changes merge in one candidate set.
Blocking output names the affected OpenTofu address, owned source file where known, and exact attribute paths without printing either side's values. If a generated resource is deleted in the controller, ubitofu removes its exact native import and resource blocks together. Any other reference, JSON import, or ambiguous import ownership blocks the deletion.
Nested values are merged only where the public manifest defines a stable
element identity. Other complex values are atomic. A non-literal HCL expression
whose source ownership cannot be proved produces typed
source_ownership_ambiguous attention. JSON HCL files (.tf.json and
.tofu.json) are indexed but read-only in 0.10, so a required JSON edit blocks.
Source files must be regular files owned by the worktree owner. Unsupported ACLs, extended attributes, file flags, symlinks, stale identities, and unsafe paths fail closed. Diagnostics name the affected relative path and the metadata fact when the operating system makes it available.
Create a private saved plan, then check that exact file immediately before an external workflow applies it:
tofu plan -out=network.tfplan
ubitofu check --plan network.tfplan --config config.toml \
--format json --output check-receipt.jsoncheck never creates a replacement plan and never writes HCL. It compares the
plan-time controller view with one fresh controller collection and reports a
digest for the supplied plan. The caller remains responsible for backing up
state, verifying that digest again, applying the same file, and removing the
private artifacts.
Without a controller revision token, a value can still change after its
endpoint was read. Secret-bearing resources have an additional limitation:
secret_freshness_unverified warns that ubitofu could not revalidate a
post-plan UI secret edit. The warning exits 0 and does not make overwriting that
secret safe. The external apply workflow must decide whether to continue.
inspect reports controller/provider coverage without writing HCL:
ubitofu inspect --config config.tomlHealth commands capture a private baseline and compare a later observation:
ubitofu health snapshot --config config.toml \
--format json --output before-health.json
ubitofu health compare --before before-health.json --config config.tomlA new degradation, a newly unknown state, a missing subsystem, or an unavailable baseline blocks. An unchanged unknown baseline is advisory.
Human and JSON output are projections of the same typed outcome. Human preview output lists decisions and changed relative paths. JSON preview output carries the same sorted paths and candidate digests without HCL or secret values.
--output PATH creates or atomically replaces an owner-owned regular file at
mode 0600. It refuses source HCL, COVERAGE.md, .ubitofu control data, the
generation scaffold, the selected config, and the command's saved-plan or
health-baseline input.
Every command uses the same exit scheme:
| Code | Meaning |
|---|---|
| 0 | success, allowed plan, or advisory warning |
| 1 | operational failure |
| 2 | command-line usage or configuration error |
| 3 | valid blocking outcome: conflict, unsafe plan, or health degradation |
Exit 0 does not mean HCL changed. An external workflow should inspect the result and the ordinary source diff. Exit 3 is a valid, fully reported outcome, not a transport or parser failure.
ubitofu never captures a secret value from the controller into HCL, output, a
receipt, or a digest. Explicit top-level secret rules render sensitive
var.<name> references. Provider-declared nested sensitive or write-only values
are suppressed and covered by lifecycle ignore policy rather than emitted.
Secret decisions retain only the path and a typed fact:
- an HCL-only secret rotation remains allowed
- a detectable controller-only secret change blocks because it cannot be captured
- different detectable changes on both sides block as a secret conflict
- equal detectable changes converge
- an unavailable or write-only value is reported as noncomparable.
Supply variable values through a secret manager, TF_VAR_*, or another
OpenTofu mechanism outside this repository.
ubitofu owns controller discovery, provider-aware generation, three-way reconciliation, structural HCL edits, saved-plan safety decisions, health interpretation, and human or JSON results.
It does not own controller writes, OpenTofu apply, state mutation or backup, deployment credentials, Git commits and pull requests, CI-vendor behavior, scheduling, or notifications. Those stay in small external tools and workflows.
See docs/testing.md for supported runtimes, local checks, controller scenarios, package verification, and the server-only mutation gate.
The repo includes .claude/skills/unifi-tofu-reconcile-workflow/ for work on a
UniFi infrastructure repository. It teaches the UI/mobile and HCL coexistence
workflow, including mandatory dry-run review before wet reconcile.
pip install ubitofu does not install the skill. Copy the directory into the
consumer repository's .claude/skills/ directory and commit it there, or copy
it into ~/.claude/skills/ for personal use.
Licensed under GPL-3.0-or-later — see LICENSE.
