Metadata-Version: 2.4
Name: pulumi-unifi-dynamic
Version: 0.1.0
Summary: A UniFi Pulumi provider as pure-Python dynamic providers — no Terraform lineage.
Project-URL: Homepage, https://github.com/hhoffman/pulumi-unifi-dynamic
Project-URL: Repository, https://github.com/hhoffman/pulumi-unifi-dynamic
Project-URL: Issues, https://github.com/hhoffman/pulumi-unifi-dynamic/issues
Project-URL: Changelog, https://github.com/hhoffman/pulumi-unifi-dynamic/blob/main/CHANGELOG.md
Author: Harry Hoffman
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: dynamic-provider,iac,networking,pulumi,ubiquiti,udm,unifi
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: System :: Networking
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.9
Requires-Dist: pulumi<4.0.0,>=3.0.0
Requires-Dist: requests>=2.31.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: responses>=0.25; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# pulumi-unifi-dynamic

[![CI](https://github.com/hhoffman/pulumi-unifi-dynamic/actions/workflows/ci.yml/badge.svg)](https://github.com/hhoffman/pulumi-unifi-dynamic/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/pulumi-unifi-dynamic.svg)](https://pypi.org/project/pulumi-unifi-dynamic/)
[![Python](https://img.shields.io/pypi/pyversions/pulumi-unifi-dynamic.svg)](https://pypi.org/project/pulumi-unifi-dynamic/)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)

Manage your **UniFi** network (UDM / UDM Pro / controller) as code with
[Pulumi](https://www.pulumi.com/) — 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](#private-api-caveat).

## Install

```bash
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:

```bash
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
```

```python
# __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)
```

```bash
pulumi preview   # read-only — no API writes
pulumi up        # writes to the live controller
```

See [`examples/basic/`](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`](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`](docs/firmware-compat.md).

## Development

```bash
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](LICENSE).
