A UniFi Pulumi provider as pure-Python dynamic providers — no Terraform lineage.
Project description
pulumi-unifi-dynamic
Manage your UniFi network (UDM / UDM Pro / controller) as code with Pulumi — written as pure-Python dynamic providers, with zero Terraform lineage (no HCL, no Terraform state, no bridged provider binary). It talks directly to the device's own API.
Not affiliated with or endorsed by Ubiquiti or Pulumi. This is a community library. "UniFi" and "Ubiquiti" are trademarks of Ubiquiti Inc.
Why this exists
The UniFi Network Integration API (local, X-API-KEY) can create and update
real config — DNS records, VLANs, firewall zones/policies, WiFi, and more — but
it's not idempotent and UUID-keyed (no upsert), which is awkward for
declarative IaC. This library wraps it in Pulumi dynamic providers that reconcile
by GET → POST/PUT, and adds the one thing dynamic providers otherwise can't do:
adopt existing config into state (below). A few surfaces the Integration API
doesn't expose (DHCP fixed-IPs, switch port profiles + their port assignments) use
the private controller API — see the caveat.
Install
pip install pulumi-unifi-dynamic
Quickstart
Connection settings come from the environment at runtime — nothing secret is ever passed as a resource input, so nothing secret is serialized into Pulumi state:
export UNIFI_API_KEY="…" # Network → Settings → Integrations → Create API Key
export UNIFI_SITE_ID="…" # GET /v1/sites → data[].id (a UUID)
export UNIFI_API_BASE="https://192.168.1.1/proxy/network/integration/v1" # optional
# TLS (see Security below): default is UNVERIFIED. To verify the UDM's self-signed cert:
# export UNIFI_CA_BUNDLE=/path/to/controller-ca.pem # recommended
# export UNIFI_VERIFY_TLS=true # only if the cert chains to a system-trusted CA
# __main__.py
from unifi_pulumi import DnsRecord, Network, FirewallZone
DnsRecord(
"app",
type="A_RECORD",
domain="app.example.com",
ipv4Address="192.0.2.10",
enabled=True,
)
Network("iot", vlan_id=20, display_name="IoT", management="GATEWAY", enabled=True)
pulumi preview # read-only — no API writes
pulumi up # writes to the live controller
See examples/basic/ for a runnable program.
The adopt-on-create model
Pulumi's native import does not work with dynamic providers. So resources
that set a natural_key (e.g. a DNS record's domain, a network's vlanId, a
zone's name) instead adopt an existing object with that key on create() —
recording its id in state without writing. This is how you bring an
already-configured UniFi under management with no churn: if the live values differ
from what you declared, the difference simply shows up as an update on the next
pulumi preview. POST only happens when no match exists.
Some resources are read_only (adoption-only) — e.g. WiFi, whose PSK the API
never returns. They adopt and drift-track but never write; use pulumi refresh to
pull live state.
Resources
| Resource | API | Notes |
|---|---|---|
DnsRecord |
Integration | A / CNAME records |
Network |
Integration | VLAN (name/enabled/vlanId; subnet/DHCP not exposed by the API) |
FirewallZone |
Integration | zone-based-firewall zones |
FirewallPolicy |
Integration | ZBF policies |
FirewallPolicyOrder |
Integration | per-zone-pair evaluation order (preserves unmanaged policies) |
TrafficMatchingList |
Integration | reusable IP/port match lists |
WiFiBroadcast |
Integration | SSIDs (read_only — PSK not returned) |
AclRule |
Integration | ACL rules |
FixedIp |
private | DHCP fixed-IP reservation |
PortProfile |
private | switch port profile |
PortProfileAssignment |
private | assign a profile to a switch port (preserves other ports) |
Private-API caveat
FixedIp, PortProfile, and PortProfileAssignment use the private/legacy
controller API (/proxy/network/api/s/<site>/rest/…) because the Integration API
has no equivalent surface. That API is undocumented and can change across
firmware — treat these as best-effort. They are written defensively (they modify
only the one object/port they own and preserve everything else), but a firmware
update could still break them; fall back to the UI if so. See
docs/unifi-private-api.md. Everything else uses the
official Integration API.
Security
- Secrets never touch Pulumi state. All credentials come from the environment
at runtime; nothing secret is passed as a resource input, so
pulumi stack exportcontains no key. The API key travels in theX-API-KEYheader, never in a URL. - TLS verification is DISABLED by default. A UDM ships a self-signed cert, so
out of the box this library does not verify it — an on-path attacker could
intercept your API key. A one-time warning is emitted. To verify (recommended,
especially off a trusted LAN):
UNIFI_CA_BUNDLE=/path/to/controller-ca.pem— verify against the controller's own cert/CA (the right fix for a self-signed UDM), orUNIFI_VERIFY_TLS=true— verify against the system trust store (only works if the controller's cert chains to a trusted CA).
- Error messages include the controller's response body (truncated) to surface its validation reason — useful for debugging, but be mindful when pasting tracebacks publicly.
- Report vulnerabilities via a private GitHub security advisory on this repo.
Compatibility
Developed and validated against UniFi Network 10.4.57 (UDM Pro). Other versions
likely work but are unverified — see docs/firmware-compat.md.
Development
pip install -e ".[dev]"
pytest # unit tests mock the HTTP layer — no live controller needed
ruff check .
Contributing & support
Best-effort, no warranty — issues and PRs welcome. Because the private-API resources track undocumented endpoints, a bug there may be a firmware change; PRs that pin the observed shape for a new firmware are especially useful.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pulumi_unifi_dynamic-0.1.0.tar.gz.
File metadata
- Download URL: pulumi_unifi_dynamic-0.1.0.tar.gz
- Upload date:
- Size: 30.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
50a84bf9ca6527e8fbc324f8c216a87c4b396a020b15de34171865c5413017a6
|
|
| MD5 |
2c9adea4f1c76dac7e52617bc9090e57
|
|
| BLAKE2b-256 |
575f4b3bb2b94891fd41b75483b72214d54f7e072ca8c5bc9096d83c60285929
|
Provenance
The following attestation bundles were made for pulumi_unifi_dynamic-0.1.0.tar.gz:
Publisher:
release.yml on hhoffman/pulumi-unifi-dynamic
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pulumi_unifi_dynamic-0.1.0.tar.gz -
Subject digest:
50a84bf9ca6527e8fbc324f8c216a87c4b396a020b15de34171865c5413017a6 - Sigstore transparency entry: 2211928361
- Sigstore integration time:
-
Permalink:
hhoffman/pulumi-unifi-dynamic@b0dc0fa9994f1491973cd2a3d667b52d00564e25 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/hhoffman
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@b0dc0fa9994f1491973cd2a3d667b52d00564e25 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pulumi_unifi_dynamic-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pulumi_unifi_dynamic-0.1.0-py3-none-any.whl
- Upload date:
- Size: 33.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
548cd5db1d2245d8864121436545cca7c28bcec3e7445a1c37b81f2a2e8dfd23
|
|
| MD5 |
aa7aa531bc54eef7ed0a275eade20757
|
|
| BLAKE2b-256 |
80f83c052e670d27a2cc2400ba4d7a55080847ed13b58c1b2313da594cb2da0d
|
Provenance
The following attestation bundles were made for pulumi_unifi_dynamic-0.1.0-py3-none-any.whl:
Publisher:
release.yml on hhoffman/pulumi-unifi-dynamic
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pulumi_unifi_dynamic-0.1.0-py3-none-any.whl -
Subject digest:
548cd5db1d2245d8864121436545cca7c28bcec3e7445a1c37b81f2a2e8dfd23 - Sigstore transparency entry: 2211928397
- Sigstore integration time:
-
Permalink:
hhoffman/pulumi-unifi-dynamic@b0dc0fa9994f1491973cd2a3d667b52d00564e25 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/hhoffman
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@b0dc0fa9994f1491973cd2a3d667b52d00564e25 -
Trigger Event:
release
-
Statement type: