From 6b53cde35ccc5976f45fc4efd6893daa572f305b Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 13 Feb 2026 20:00:13 -0800 Subject: [PATCH] Update UniFi Pulumi plan: switch to ubiquiti-community provider Corroboration review of the add-unifi-pulumi-stack plan found several issues. Switch provider from filipowm/unifi (inactive maintainer, showstopper bug #94 wiping firewall rules) to ubiquiti-community/unifi (actively maintained, API key auth). Add UX7 config backup prerequisite, fix safety guard to check default route instead of hostname, update 1Password paths to match actual item, fix ringtail references, and update doc steps for already-existing files. Co-Authored-By: Claude Opus 4.6 --- docs/how-to/plans/add-unifi-pulumi-stack.md | 80 ++++++++++++--------- docs/reference/infrastructure/unifi.md | 5 +- 2 files changed, 49 insertions(+), 36 deletions(-) diff --git a/docs/how-to/plans/add-unifi-pulumi-stack.md b/docs/how-to/plans/add-unifi-pulumi-stack.md index 2359b43..58847e3 100644 --- a/docs/how-to/plans/add-unifi-pulumi-stack.md +++ b/docs/how-to/plans/add-unifi-pulumi-stack.md @@ -1,6 +1,6 @@ --- title: "Plan: Add UniFi Pulumi Stack" -modified: 2026-02-11 +modified: 2026-02-13 tags: - how-to - plans @@ -11,7 +11,7 @@ tags: # Plan: Add UniFi Pulumi Stack > **Status:** Planned (not yet executed) -> **Blocked by:** Ethernet switch purchase and cabling +> **Blocked by:** 1Password credential setup (API key) ## Background @@ -24,13 +24,13 @@ The UniFi Express 7 (UX7) is the home WiFi router, currently unmanaged. This pla - **Consistency** — joins the existing Pulumi stacks for Tailscale ACLs and DNS - **Network segmentation** — declare main/guest/IoT WiFi networks with proper firewall zones -### Why This Is Blocked +### Ethernet Requirement (Resolved) -The UX7 has one LAN port, currently connected to [[sifaka]]. Modifying WiFi settings over WiFi would sever the management connection mid-apply. We need: +The UX7 has one LAN port. Modifying WiFi settings over WiFi would sever the management connection mid-apply. This was resolved by installing: 1. **Two switches** (UniFi Switch Flex Mini recommended) daisy-chained: - Switch A by the router: connects UX7, sifaka - - Switch B on the desk (~12ft cable): connects indri, ringtail, and optionally gilbert + - Switch B on the desk (~12ft cable): connects indri and gilbert 2. **Cat6 Ethernet cables**: one ~12ft run between switches, plus short cables for each device ``` @@ -39,8 +39,7 @@ UniFi Express 7 [LAN port] ├── sifaka (short cable) └── ~12ft Cat6 ──→ Switch B (on desk) ├── indri (Cat6) - ├── ringtail (Cat6) - └── (gilbert via USB-C adapter, optional) + └── gilbert (USB-C adapter) ``` Daisy-chaining is standard Layer 2 networking — no speed loss per device, no subnet impact. The only shared bottleneck is the 1 Gbps uplink between the two switches, which is more than adequate for homelab use. UniFi Flex Minis will appear in the UX7's controller for monitoring and eventual Pulumi management. @@ -52,25 +51,38 @@ Before starting the execution session: - [ ] Purchase 2x UniFi Switch Flex Mini (USW-Flex-Mini) - [ ] Purchase Cat6 cables: 1x ~12ft, 3-4x short (~3ft) - [ ] Cable everything up and verify all devices have network connectivity -- [ ] Verify indri has an active wired Ethernet connection: `networksetup -getinfo "Ethernet"` should show an IP address +- [ ] Verify the machine running Pulumi (gilbert) has an active wired Ethernet connection as its default route: `route -n get default` should show a non-Wi-Fi interface +- [ ] **Back up the UX7 configuration** via `https://192.168.1.1` → Settings → System → Backup, and download a `.unf` backup file. Store it safely before making any IaC changes. This provides a rollback path if a provider bug corrupts network or firewall state. - [ ] Create an API key on the UX7 via `https://192.168.1.1` → Settings → Control Plane → API (preferred over username/password for IaC) -- [ ] Store UniFi credentials in 1Password: vault `blumeops`, item `unifi - blumeops`, fields `api_key` and `url` +- [ ] Store UniFi API key in 1Password: vault `blumeops`, item `unifi`, category `API_CREDENTIAL` - [ ] Verify Pulumi CLI version is >= v3.147.0 (`pulumi version`) -## Provider: filipowm/unifi via `pulumi package add` +## Provider: ubiquiti-community/unifi via `pulumi package add` -We use `pulumi package add terraform-provider filipowm/unifi 1.0.0` to consume the [filipowm fork](https://github.com/filipowm/terraform-provider-unifi) of the UniFi Terraform provider directly from Pulumi. This approach: +We use `pulumi package add terraform-provider ubiquiti-community/unifi` to consume the [ubiquiti-community fork](https://github.com/ubiquiti-community/terraform-provider-unifi) of the UniFi Terraform provider directly from Pulumi. This approach: - **Generates a local Python SDK** in `./sdks/unifi/` and adds a package reference to `Pulumi.yaml` -- **Supports zone-based firewalls** — needed for main/guest/IoT WiFi segmentation and a dedicated blumeops services subnet - **Supports API key authentication** — cleaner than username/password for IaC -- **Actively maintained** — v1.0.0, 520+ commits, not the abandoned upstream +- **Actively maintained** — v0.41.12 (Jan 2026), responsive maintainer, 12 releases since Oct 2025 - **Uses Pulumi Cloud state** — same as existing stacks, free tier - **No fork/bridge maintenance** — just re-run `pulumi package add` on new provider versions +- **Broader ecosystem** — part of the [ubiquiti-community](https://github.com/ubiquiti-community) org alongside go-unifi, unifi-api, and other tools -### Why Not pulumiverse_unifi? +### Why Not Other Providers? -The `pulumiverse_unifi` PyPI package bridges from `paultyng/terraform-provider-unifi`, which is in maintenance mode. It lacks zone-based firewalls, API key auth, and many newer resource types. The filipowm fork is the active community successor. +| Provider | Why Not | +|----------|---------| +| `pulumiverse_unifi` | Bridges `paultyng/terraform-provider-unifi`, which is abandoned. No API key auth, no newer resource types. | +| `filipowm/unifi` | Maintainer unresponsive since April 2025. Critical bug ([#94](https://github.com/filipowm/terraform-provider-unifi/issues/94)): applying `unifi_network` resources wipes all zone-based firewall rules. Unmerged community fix PRs. | +| `paultyng/unifi` | Abandoned since March 2023. No API key auth, no zone-based firewall. | + +### Zone-Based Firewall: Deferred + +The ubiquiti-community provider does not yet support zone-based firewall resources ([#77](https://github.com/ubiquiti-community/terraform-provider-unifi/issues/77)). Zone-based firewall rules will be managed manually in the UX7 web UI until provider support lands. This is acceptable because: + +- The initial goal is bringing networks, WLANs, and DHCP under IaC +- Network segmentation (which needs firewall zones) is a future phase +- The filipowm provider — the only one with zone firewall support — has a showstopper bug that makes it unusable for this purpose anyway ## Network Segmentation Goals @@ -124,7 +136,7 @@ Following the conventions of `pulumi/tailscale/` and `pulumi/gandi/`: ``` pulumi/unifi/ ├── Pulumi.yaml # name: blumeops-unifi, python runtime, uv toolchain -│ # includes parameterized package reference for filipowm/unifi +│ # includes parameterized package reference for ubiquiti-community/unifi ├── Pulumi.home-network.yaml # Stack config: router_url, site ├── pyproject.toml # Python >=3.11, pulumi>=3.0.0 ├── sdks/unifi/ # Generated Python SDK from pulumi package add @@ -140,18 +152,21 @@ pulumi/unifi/ | Variable | Value | Notes | |----------|-------|-------| -| `UNIFI_API_KEY` | from 1Password | API key created in UX7 control plane | -| `UNIFI_API` | `https://192.168.1.1:443` | No `/api` suffix — SDK auto-discovers `/proxy/network` for UniFi OS | +| `UNIFI_API_KEY` | `op read "op://blumeops/unifi/credential"` | API key created in UX7 control plane | +| `UNIFI_API` | `https://192.168.1.1` | No `/api` suffix — SDK auto-discovers `/proxy/network` for UniFi OS | | `UNIFI_INSECURE` | `true` | UX7 uses a self-signed TLS certificate | ### Safety Guard -The `__main__.py` must fail fast before creating any Pulumi resources if: +The `__main__.py` must fail fast before creating any Pulumi resources if the default network route goes through Wi-Fi. This prevents accidentally modifying WiFi settings while connected over WiFi (which would sever the management connection mid-apply). -1. **Wrong host** — `platform.node()` is not `indri` -2. **No wired connection** — `networksetup -getinfo "Ethernet"` shows no active Ethernet +The check works as follows: -This prevents accidentally running the stack from gilbert over WiFi. +1. Run `route -n get default` and extract the `interface:` field (e.g., `en5`) +2. Run `networksetup -listallhardwareports` and find which hardware port owns that interface +3. If the hardware port is `Wi-Fi`, abort with an error + +This is host-agnostic — it works on both gilbert (where the Ethernet adapter is `AX88179A` on `en5`) and indri (where it's `Ethernet` on `en0`). ## Execution Steps @@ -161,7 +176,7 @@ This prevents accidentally running the stack from gilbert over WiFi. mkdir -p pulumi/unifi cd pulumi/unifi # Create Pulumi.yaml, pyproject.toml, .gitignore, __main__.py -pulumi package add terraform-provider filipowm/unifi 1.0.0 +pulumi package add terraform-provider ubiquiti-community/unifi # This generates sdks/unifi/ and updates Pulumi.yaml with package reference pulumi install uv sync @@ -171,7 +186,7 @@ uv sync Create `Pulumi.yaml`, `Pulumi.home-network.yaml`, `pyproject.toml`, `.gitignore`, and `__main__.py`. The main program should declare: -- **Ethernet safety guard** (hostname + wired connection check) +- **Ethernet safety guard** (verify default route is not Wi-Fi) - **Default LAN network** resource (corporate, `192.168.1.0/24`, DHCP) - **WiFi WLAN** resources (commented out initially — need SSID names and IDs from the controller) - **Exports** for router IP, network ID, subnet @@ -181,12 +196,12 @@ Create `Pulumi.yaml`, `Pulumi.home-network.yaml`, `pyproject.toml`, `.gitignore` Create `mise-tasks/unifi-preview` and `mise-tasks/unifi-up` following the pattern from `tailnet-up`/`dns-up`: ```bash -UNIFI_API_KEY=$(op read "op://blumeops/unifi - blumeops/api_key") -export UNIFI_API="https://192.168.1.1:443" +UNIFI_API_KEY=$(op read "op://blumeops/unifi/credential") +export UNIFI_API="https://192.168.1.1" export UNIFI_INSECURE="true" ``` -### Step 4: Initialize Stack (on indri) +### Step 4: Initialize Stack ```fish cd pulumi/unifi @@ -210,9 +225,7 @@ Adjust `__main__.py` resource properties to match the actual controller state un ### Step 6: Documentation Updates -- Create `docs/reference/infrastructure/unifi.md` reference card -- Update `docs/reference/infrastructure/hosts.md` — link UniFi row to `[[unifi|Details]]` -- Update `docs/reference/reference.md` — add `[[unifi]]` to Infrastructure section +- Update `docs/reference/infrastructure/unifi.md` — remove `(planned)` markers, update provider to ubiquiti-community - Add changelog fragment ### Step 7: Verify @@ -223,15 +236,16 @@ Adjust `__main__.py` resource properties to match the actual controller state un ## Known Limitations -- **macOS-specific guard** — the `networksetup` check only works on macOS, which is fine since indri is permanently macOS +- **macOS-specific guard** — the `networksetup` and `route` checks only work on macOS, which is fine since the stack is run from gilbert or indri, both permanently macOS - **User group ID discovery** — the provider may not expose a `get_user_group` data source. Must be discovered manually from the controller API (`/proxy/network/api/s/default/rest/usergroup`) and hardcoded ## Future Considerations -- **Firewall rules** — declare zone-based firewall rules after the initial import is stable +- **Zone-based firewall rules** — manage via Pulumi once ubiquiti-community adds support ([#77](https://github.com/ubiquiti-community/terraform-provider-unifi/issues/77)). Until then, configure manually in the UX7 web UI. +- **Network segmentation** — depends on zone-based firewall support; see goals above - **UnPoller** — add Prometheus metrics exporter for UniFi gear, integrates with existing Grafana stack - **Switch management** — manage the USW-Flex-Minis via the same Pulumi stack once adopted into the UX7 controller -- **Provider updates** — re-run `pulumi package add terraform-provider filipowm/unifi ` to update +- **Provider updates** — re-run `pulumi package add terraform-provider ubiquiti-community/unifi` to update ## Reference Pattern Files diff --git a/docs/reference/infrastructure/unifi.md b/docs/reference/infrastructure/unifi.md index dd2fc0f..3d64a61 100644 --- a/docs/reference/infrastructure/unifi.md +++ b/docs/reference/infrastructure/unifi.md @@ -39,8 +39,7 @@ ISP Modem ├── sifaka (Synology NAS) └── ~12ft Cat6 ──→ Switch B (on desk) ├── indri (Mac Mini, primary server) - ├── ringtail (Raspberry Pi) - └── (gilbert via USB-C adapter, optional) + └── gilbert (USB-C adapter) ``` All wired devices share the `192.168.1.0/24` subnet. The two daisy-chained UniFi Switch Flex Minis provide enough ports for all devices while using the UX7's single LAN port. @@ -67,7 +66,7 @@ See [[add-unifi-pulumi-stack]] for the full implementation plan. ## Authentication -The provider uses an API key created in the UX7 control plane (Settings → Control Plane → API). The key is stored in 1Password (`op://blumeops/unifi - blumeops/api_key`) and injected via mise task environment variables. +The provider uses an API key created in the UX7 control plane (Settings → Control Plane → API). The key is stored in 1Password (`op://blumeops/unifi/credential`) and injected via mise task environment variables. ## Related