Skip to main content

A UniFi Pulumi provider as pure-Python dynamic providers — no Terraform lineage.

Project description

pulumi-unifi-dynamic

CI PyPI Python License

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 export contains no key. The API key travels in the X-API-KEY header, 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), or
    • UNIFI_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

Apache-2.0.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pulumi_unifi_dynamic-0.1.0.tar.gz (30.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pulumi_unifi_dynamic-0.1.0-py3-none-any.whl (33.4 kB view details)

Uploaded Python 3

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

Hashes for pulumi_unifi_dynamic-0.1.0.tar.gz
Algorithm Hash digest
SHA256 50a84bf9ca6527e8fbc324f8c216a87c4b396a020b15de34171865c5413017a6
MD5 2c9adea4f1c76dac7e52617bc9090e57
BLAKE2b-256 575f4b3bb2b94891fd41b75483b72214d54f7e072ca8c5bc9096d83c60285929

See more details on using hashes here.

Provenance

The following attestation bundles were made for pulumi_unifi_dynamic-0.1.0.tar.gz:

Publisher: release.yml on hhoffman/pulumi-unifi-dynamic

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pulumi_unifi_dynamic-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pulumi_unifi_dynamic-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 548cd5db1d2245d8864121436545cca7c28bcec3e7445a1c37b81f2a2e8dfd23
MD5 aa7aa531bc54eef7ed0a275eade20757
BLAKE2b-256 80f83c052e670d27a2cc2400ba4d7a55080847ed13b58c1b2313da594cb2da0d

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page