From 6b0005df1eba6d13204b8534f2aec846d2c362c8 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Mon, 16 Mar 2026 13:56:24 -0700 Subject: [PATCH 1/8] Add UnPoller deployment for UniFi network metrics Deploy UnPoller as a k8s service on indri to export UniFi controller metrics to Prometheus. Custom-built container from forge mirror, with credentials pulled from 1Password via external-secrets. Co-Authored-By: Claude Opus 4.6 (1M context) --- argocd/apps/unpoller.yaml | 17 +++++++ argocd/manifests/prometheus/prometheus.yml | 8 ++++ argocd/manifests/unpoller/deployment.yaml | 47 +++++++++++++++++++ .../manifests/unpoller/external-secret.yaml | 22 +++++++++ argocd/manifests/unpoller/kustomization.yaml | 18 +++++++ argocd/manifests/unpoller/service.yaml | 13 +++++ argocd/manifests/unpoller/up.conf | 17 +++++++ containers/unpoller/Dockerfile | 40 ++++++++++++++++ docs/changelog.d/feature-unpoller.feature.md | 1 + docs/reference/infrastructure/unifi.md | 10 +++- service-versions.yaml | 7 +++ 11 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 argocd/apps/unpoller.yaml create mode 100644 argocd/manifests/unpoller/deployment.yaml create mode 100644 argocd/manifests/unpoller/external-secret.yaml create mode 100644 argocd/manifests/unpoller/kustomization.yaml create mode 100644 argocd/manifests/unpoller/service.yaml create mode 100644 argocd/manifests/unpoller/up.conf create mode 100644 containers/unpoller/Dockerfile create mode 100644 docs/changelog.d/feature-unpoller.feature.md diff --git a/argocd/apps/unpoller.yaml b/argocd/apps/unpoller.yaml new file mode 100644 index 0000000..5eaadfb --- /dev/null +++ b/argocd/apps/unpoller.yaml @@ -0,0 +1,17 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: unpoller + namespace: argocd +spec: + project: default + source: + repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + path: argocd/manifests/unpoller + destination: + server: https://kubernetes.default.svc + namespace: monitoring + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/argocd/manifests/prometheus/prometheus.yml b/argocd/manifests/prometheus/prometheus.yml index 2fd3252..2d2dbcf 100644 --- a/argocd/manifests/prometheus/prometheus.yml +++ b/argocd/manifests/prometheus/prometheus.yml @@ -72,6 +72,14 @@ scrape_configs: - target_label: cluster replacement: indri + # UniFi network metrics (via UnPoller exporter) + - job_name: "unpoller" + static_configs: + - targets: ["unpoller.monitoring.svc.cluster.local:9130"] + metric_relabel_configs: + - target_label: cluster + replacement: indri + # Frigate NVR metrics (via Caddy on indri — Frigate runs on ringtail) - job_name: "frigate" scheme: https diff --git a/argocd/manifests/unpoller/deployment.yaml b/argocd/manifests/unpoller/deployment.yaml new file mode 100644 index 0000000..cffa704 --- /dev/null +++ b/argocd/manifests/unpoller/deployment.yaml @@ -0,0 +1,47 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: unpoller + namespace: monitoring + labels: + app: unpoller +spec: + replicas: 1 + selector: + matchLabels: + app: unpoller + template: + metadata: + labels: + app: unpoller + spec: + containers: + - name: unpoller + image: registry.ops.eblu.me/blumeops/unpoller:kustomized + ports: + - containerPort: 9130 + name: metrics + env: + - name: UP_UNIFI_DEFAULT_USER + valueFrom: + secretKeyRef: + name: unpoller-unifi + key: username + - name: UP_UNIFI_DEFAULT_PASS + valueFrom: + secretKeyRef: + name: unpoller-unifi + key: password + volumeMounts: + - name: config + mountPath: /etc/unpoller + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + memory: 128Mi + volumes: + - name: config + configMap: + name: unpoller-config diff --git a/argocd/manifests/unpoller/external-secret.yaml b/argocd/manifests/unpoller/external-secret.yaml new file mode 100644 index 0000000..e4c3165 --- /dev/null +++ b/argocd/manifests/unpoller/external-secret.yaml @@ -0,0 +1,22 @@ +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: unpoller-unifi + namespace: monitoring +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-blumeops + target: + name: unpoller-unifi + creationPolicy: Owner + data: + - secretKey: username + remoteRef: + key: unifi + property: username + - secretKey: password + remoteRef: + key: unifi + property: password diff --git a/argocd/manifests/unpoller/kustomization.yaml b/argocd/manifests/unpoller/kustomization.yaml new file mode 100644 index 0000000..3951e68 --- /dev/null +++ b/argocd/manifests/unpoller/kustomization.yaml @@ -0,0 +1,18 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: monitoring + +resources: + - deployment.yaml + - service.yaml + - external-secret.yaml + +images: + - name: registry.ops.eblu.me/blumeops/unpoller + newTag: v2.34.0 + +configMapGenerator: + - name: unpoller-config + files: + - up.conf diff --git a/argocd/manifests/unpoller/service.yaml b/argocd/manifests/unpoller/service.yaml new file mode 100644 index 0000000..1ce870b --- /dev/null +++ b/argocd/manifests/unpoller/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: unpoller + namespace: monitoring +spec: + selector: + app: unpoller + ports: + - port: 9130 + targetPort: metrics + protocol: TCP + name: metrics diff --git a/argocd/manifests/unpoller/up.conf b/argocd/manifests/unpoller/up.conf new file mode 100644 index 0000000..cafef5b --- /dev/null +++ b/argocd/manifests/unpoller/up.conf @@ -0,0 +1,17 @@ +[prometheus] + http_listen = "0.0.0.0:9130" + report_errors = true + +[influxdb] + disable = true + +[unifi] + dynamic = false + +[unifi.defaults] + # Credentials come from environment variables: + # UP_UNIFI_DEFAULT_USER and UP_UNIFI_DEFAULT_PASS + url = "https://192.168.1.1" + verify_ssl = false + save_sites = true + save_dpi = false diff --git a/containers/unpoller/Dockerfile b/containers/unpoller/Dockerfile new file mode 100644 index 0000000..0391f6d --- /dev/null +++ b/containers/unpoller/Dockerfile @@ -0,0 +1,40 @@ +# UnPoller — UniFi metrics exporter for Prometheus +# Two-stage build: Go compilation, then minimal Alpine runtime + +ARG CONTAINER_APP_VERSION=v2.34.0 + +FROM golang:alpine3.22 AS build + +ARG CONTAINER_APP_VERSION +RUN apk add --no-cache git + +RUN git clone --depth 1 --branch ${CONTAINER_APP_VERSION} \ + https://forge.ops.eblu.me/mirrors/unpoller.git /app + +WORKDIR /app + +ENV CGO_ENABLED=0 + +RUN go build -ldflags="-s -w \ + -X main.version=${CONTAINER_APP_VERSION} \ + -X main.builtBy=blumeops \ + -X golift.io/version.Version=${CONTAINER_APP_VERSION} \ + -X golift.io/version.Branch=HEAD \ + -X golift.io/version.BuildUser=blumeops \ + -X golift.io/version.Revision=blumeops-build" \ + -o /bin/unpoller . + +FROM alpine:3.22 + +LABEL org.opencontainers.image.title="UnPoller" +LABEL org.opencontainers.image.description="UniFi metrics exporter for Prometheus" +LABEL org.opencontainers.image.source="https://github.com/unpoller/unpoller" + +RUN apk add --no-cache ca-certificates tzdata + +COPY --from=build /bin/unpoller /usr/bin/unpoller + +EXPOSE 9130 +USER 65534:65534 +ENTRYPOINT ["/usr/bin/unpoller"] +CMD ["--config", "/etc/unpoller/up.conf"] diff --git a/docs/changelog.d/feature-unpoller.feature.md b/docs/changelog.d/feature-unpoller.feature.md new file mode 100644 index 0000000..848cbbc --- /dev/null +++ b/docs/changelog.d/feature-unpoller.feature.md @@ -0,0 +1 @@ +Add UnPoller deployment to monitor UniFi network metrics via Prometheus diff --git a/docs/reference/infrastructure/unifi.md b/docs/reference/infrastructure/unifi.md index 71ca744..d02604f 100644 --- a/docs/reference/infrastructure/unifi.md +++ b/docs/reference/infrastructure/unifi.md @@ -1,6 +1,6 @@ --- title: UniFi -modified: 2026-02-24 +modified: 2026-03-16 tags: - infrastructure - networking @@ -69,6 +69,14 @@ Local admin account on the UX7. Credentials stored in 1Password (vault `blumeops Attempted Feb 2026 with the `ubiquiti-community/unifi` Terraform provider via Pulumi. A "no-op" update on the default LAN network reset undeclared properties, bricking the network and requiring a factory reset. The provider ecosystem is too immature for single-device infrastructure. +## Monitoring + +UniFi metrics are exported to Prometheus via [UnPoller](https://github.com/unpoller/unpoller), running as a k8s deployment in the `monitoring` namespace on indri. UnPoller polls the UX7 controller API using a read-only local account (`blumeops`) and exposes metrics on port 9130. + +- **Prometheus job:** `unpoller` +- **Metrics prefix:** `unifi_` +- **Credentials:** 1Password item `unifi` (vault `blumeops`) + ## Related - [[hosts]] — Device inventory diff --git a/service-versions.yaml b/service-versions.yaml index 0ad1733..686a529 100644 --- a/service-versions.yaml +++ b/service-versions.yaml @@ -253,6 +253,13 @@ services: upstream-source: https://code.forgejo.org/forgejo/runner/releases notes: Forgejo runner on ringtail via nixpkgs; version tracks flake.lock + - name: unpoller + type: argocd + last-reviewed: 2026-03-16 + current-version: "v2.34.0" + upstream-source: https://github.com/unpoller/unpoller/releases + notes: UniFi metrics exporter for Prometheus + - name: forgejo type: ansible last-reviewed: 2026-02-22 From 8a8bfffdb1de61c1180427dd1408658a0b799cfa Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Mon, 16 Mar 2026 14:00:56 -0700 Subject: [PATCH 2/8] Point unpoller kustomization at built container tag Co-Authored-By: Claude Opus 4.6 (1M context) --- argocd/manifests/unpoller/kustomization.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/argocd/manifests/unpoller/kustomization.yaml b/argocd/manifests/unpoller/kustomization.yaml index 3951e68..da7524d 100644 --- a/argocd/manifests/unpoller/kustomization.yaml +++ b/argocd/manifests/unpoller/kustomization.yaml @@ -10,7 +10,7 @@ resources: images: - name: registry.ops.eblu.me/blumeops/unpoller - newTag: v2.34.0 + newTag: v2.34.0-6b0005d configMapGenerator: - name: unpoller-config From 74c8ef7209ac0ccb9d4c23db11987b22638ff91a Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Mon, 16 Mar 2026 14:42:03 -0700 Subject: [PATCH 3/8] Switch unpoller to API key auth from 1Password item 'unpoller' Co-Authored-By: Claude Opus 4.6 (1M context) --- argocd/manifests/unpoller/deployment.yaml | 9 ++------- argocd/manifests/unpoller/external-secret.yaml | 10 +++------- argocd/manifests/unpoller/up.conf | 3 +-- docs/reference/infrastructure/unifi.md | 4 ++-- 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/argocd/manifests/unpoller/deployment.yaml b/argocd/manifests/unpoller/deployment.yaml index cffa704..2f7d13c 100644 --- a/argocd/manifests/unpoller/deployment.yaml +++ b/argocd/manifests/unpoller/deployment.yaml @@ -22,16 +22,11 @@ spec: - containerPort: 9130 name: metrics env: - - name: UP_UNIFI_DEFAULT_USER + - name: UP_UNIFI_DEFAULT_API_KEY valueFrom: secretKeyRef: name: unpoller-unifi - key: username - - name: UP_UNIFI_DEFAULT_PASS - valueFrom: - secretKeyRef: - name: unpoller-unifi - key: password + key: api-key volumeMounts: - name: config mountPath: /etc/unpoller diff --git a/argocd/manifests/unpoller/external-secret.yaml b/argocd/manifests/unpoller/external-secret.yaml index e4c3165..c82ec0d 100644 --- a/argocd/manifests/unpoller/external-secret.yaml +++ b/argocd/manifests/unpoller/external-secret.yaml @@ -12,11 +12,7 @@ spec: name: unpoller-unifi creationPolicy: Owner data: - - secretKey: username + - secretKey: api-key remoteRef: - key: unifi - property: username - - secretKey: password - remoteRef: - key: unifi - property: password + key: unpoller + property: credential diff --git a/argocd/manifests/unpoller/up.conf b/argocd/manifests/unpoller/up.conf index cafef5b..0430067 100644 --- a/argocd/manifests/unpoller/up.conf +++ b/argocd/manifests/unpoller/up.conf @@ -9,8 +9,7 @@ dynamic = false [unifi.defaults] - # Credentials come from environment variables: - # UP_UNIFI_DEFAULT_USER and UP_UNIFI_DEFAULT_PASS + # API key comes from environment variable: UP_UNIFI_DEFAULT_API_KEY url = "https://192.168.1.1" verify_ssl = false save_sites = true diff --git a/docs/reference/infrastructure/unifi.md b/docs/reference/infrastructure/unifi.md index d02604f..6182880 100644 --- a/docs/reference/infrastructure/unifi.md +++ b/docs/reference/infrastructure/unifi.md @@ -71,11 +71,11 @@ Attempted Feb 2026 with the `ubiquiti-community/unifi` Terraform provider via Pu ## Monitoring -UniFi metrics are exported to Prometheus via [UnPoller](https://github.com/unpoller/unpoller), running as a k8s deployment in the `monitoring` namespace on indri. UnPoller polls the UX7 controller API using a read-only local account (`blumeops`) and exposes metrics on port 9130. +UniFi metrics are exported to Prometheus via [UnPoller](https://github.com/unpoller/unpoller), running as a k8s deployment in the `monitoring` namespace on indri. UnPoller polls the UX7 controller API using an API key and exposes metrics on port 9130. - **Prometheus job:** `unpoller` - **Metrics prefix:** `unifi_` -- **Credentials:** 1Password item `unifi` (vault `blumeops`) +- **Credentials:** 1Password item `unpoller` (vault `blumeops`, API key) ## Related From 61b1e0cc07d37e22c36ca3be36a046a4d1371084 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Mon, 16 Mar 2026 14:50:35 -0700 Subject: [PATCH 4/8] Switch unpoller to remote API mode via api.ui.com Local API key auth doesn't work with the UX7's new Integration API. Use the remote Site Manager API instead, which auto-discovers consoles. Events disabled due to upstream bug (unpoller/unpoller#966). Co-Authored-By: Claude Opus 4.6 (1M context) --- argocd/manifests/unpoller/deployment.yaml | 2 +- argocd/manifests/unpoller/up.conf | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/argocd/manifests/unpoller/deployment.yaml b/argocd/manifests/unpoller/deployment.yaml index 2f7d13c..8fcd0fa 100644 --- a/argocd/manifests/unpoller/deployment.yaml +++ b/argocd/manifests/unpoller/deployment.yaml @@ -22,7 +22,7 @@ spec: - containerPort: 9130 name: metrics env: - - name: UP_UNIFI_DEFAULT_API_KEY + - name: UP_UNIFI_REMOTE_API_KEY valueFrom: secretKeyRef: name: unpoller-unifi diff --git a/argocd/manifests/unpoller/up.conf b/argocd/manifests/unpoller/up.conf index 0430067..ede751a 100644 --- a/argocd/manifests/unpoller/up.conf +++ b/argocd/manifests/unpoller/up.conf @@ -7,10 +7,12 @@ [unifi] dynamic = false + # Remote API mode — auto-discovers consoles via api.ui.com + remote = true + # API key comes from environment variable: UP_UNIFI_REMOTE_API_KEY [unifi.defaults] - # API key comes from environment variable: UP_UNIFI_DEFAULT_API_KEY - url = "https://192.168.1.1" - verify_ssl = false + verify_ssl = true save_sites = true save_dpi = false + save_events = false From 6916c5b5eeffbbeaad586e61a9a56ef4ad26312f Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Mon, 16 Mar 2026 15:27:45 -0700 Subject: [PATCH 5/8] Switch unpoller back to local API key auth The UX7's local Integration API key works against both the new Integration API and the classic controller API. Remote mode not needed. Co-Authored-By: Claude Opus 4.6 (1M context) --- argocd/manifests/unpoller/deployment.yaml | 2 +- argocd/manifests/unpoller/up.conf | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/argocd/manifests/unpoller/deployment.yaml b/argocd/manifests/unpoller/deployment.yaml index 8fcd0fa..2f7d13c 100644 --- a/argocd/manifests/unpoller/deployment.yaml +++ b/argocd/manifests/unpoller/deployment.yaml @@ -22,7 +22,7 @@ spec: - containerPort: 9130 name: metrics env: - - name: UP_UNIFI_REMOTE_API_KEY + - name: UP_UNIFI_DEFAULT_API_KEY valueFrom: secretKeyRef: name: unpoller-unifi diff --git a/argocd/manifests/unpoller/up.conf b/argocd/manifests/unpoller/up.conf index ede751a..0430067 100644 --- a/argocd/manifests/unpoller/up.conf +++ b/argocd/manifests/unpoller/up.conf @@ -7,12 +7,10 @@ [unifi] dynamic = false - # Remote API mode — auto-discovers consoles via api.ui.com - remote = true - # API key comes from environment variable: UP_UNIFI_REMOTE_API_KEY [unifi.defaults] - verify_ssl = true + # API key comes from environment variable: UP_UNIFI_DEFAULT_API_KEY + url = "https://192.168.1.1" + verify_ssl = false save_sites = true save_dpi = false - save_events = false From 0631035a43f279e799b3279ef768b68d96dc47f4 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Mon, 16 Mar 2026 15:43:55 -0700 Subject: [PATCH 6/8] Add UnPoller Grafana dashboards via initcontainer Fetches 6 Prometheus dashboards from the forge-mirrored unpoller/dashboards repo at pod startup, matching the TeslaMate dashboard pattern. Co-Authored-By: Claude Opus 4.6 (1M context) --- argocd/manifests/grafana/deployment.yaml | 32 ++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/argocd/manifests/grafana/deployment.yaml b/argocd/manifests/grafana/deployment.yaml index bdf4a6f..74315d7 100644 --- a/argocd/manifests/grafana/deployment.yaml +++ b/argocd/manifests/grafana/deployment.yaml @@ -90,6 +90,38 @@ spec: volumeMounts: - name: sc-dashboard-volume mountPath: /tmp/dashboards + # Fetch UnPoller (UniFi) dashboards from forge mirror. + # Source: github.com/unpoller/dashboards (v2.0.0 Prometheus set) + - name: init-unpoller-dashboards + image: docker.io/library/alpine:kustomized + imagePullPolicy: IfNotPresent + command: ["sh", "-c"] + args: + - | + set -e + BASE_URL="https://forge.ops.eblu.me/mirrors/unpoller-dashboards/raw/branch/master/v2.0.0" + DEST="/tmp/dashboards/UniFi" + mkdir -p "$DEST" + for f in \ + "UniFi-Poller_ Client DPI - Prometheus.json" \ + "UniFi-Poller_ Client Insights - Prometheus.json" \ + "UniFi-Poller_ Network Sites - Prometheus.json" \ + "UniFi-Poller_ UAP Insights - Prometheus.json" \ + "UniFi-Poller_ USG Insights - Prometheus.json" \ + "UniFi-Poller_ USW Insights - Prometheus.json" \ + ; do + wget -q -O "$DEST/$f" "$BASE_URL/$(echo "$f" | sed 's/ /%20/g')" + done + echo "Fetched $(ls "$DEST" | wc -l) UnPoller dashboards" + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + volumeMounts: + - name: sc-dashboard-volume + mountPath: /tmp/dashboards containers: # Dashboard sidecar - watches ConfigMaps with grafana_dashboard=1 - name: grafana-sc-dashboard From a39b9a4bf6ada90eb7e75f9181af0985ba303a9a Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Mon, 16 Mar 2026 15:48:24 -0700 Subject: [PATCH 7/8] Fix UnPoller dashboard datasource UIDs, drop DPI dashboard Upstream dashboards hardcode a different Prometheus datasource UID. Patch them at download time to match our 'prometheus' UID. Remove the Client DPI dashboard since DPI is not enabled on the UX7. Co-Authored-By: Claude Opus 4.6 (1M context) --- argocd/manifests/grafana/deployment.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/argocd/manifests/grafana/deployment.yaml b/argocd/manifests/grafana/deployment.yaml index 74315d7..9172991 100644 --- a/argocd/manifests/grafana/deployment.yaml +++ b/argocd/manifests/grafana/deployment.yaml @@ -103,7 +103,6 @@ spec: DEST="/tmp/dashboards/UniFi" mkdir -p "$DEST" for f in \ - "UniFi-Poller_ Client DPI - Prometheus.json" \ "UniFi-Poller_ Client Insights - Prometheus.json" \ "UniFi-Poller_ Network Sites - Prometheus.json" \ "UniFi-Poller_ UAP Insights - Prometheus.json" \ @@ -112,6 +111,9 @@ spec: ; do wget -q -O "$DEST/$f" "$BASE_URL/$(echo "$f" | sed 's/ /%20/g')" done + # Fix datasource UIDs to match our Prometheus instance + sed -i 's/"uid": *"bdkj55oguty4gd"/"uid": "prometheus"/g' "$DEST"/*.json + sed -i 's/"uid": *"\${DS_PROMETHEUS}"/"uid": "prometheus"/g' "$DEST"/*.json echo "Fetched $(ls "$DEST" | wc -l) UnPoller dashboards" securityContext: allowPrivilegeEscalation: false From fcabbddbb803a7d0d6e8266b25531ffad1053457 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Mon, 16 Mar 2026 15:49:12 -0700 Subject: [PATCH 8/8] Comment out DPI dashboard with note instead of removing Co-Authored-By: Claude Opus 4.6 (1M context) --- argocd/manifests/grafana/deployment.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/argocd/manifests/grafana/deployment.yaml b/argocd/manifests/grafana/deployment.yaml index 9172991..130618c 100644 --- a/argocd/manifests/grafana/deployment.yaml +++ b/argocd/manifests/grafana/deployment.yaml @@ -103,6 +103,8 @@ spec: DEST="/tmp/dashboards/UniFi" mkdir -p "$DEST" for f in \ + # DPI dashboard requires DPI enabled on both UX7 and UnPoller + # "UniFi-Poller_ Client DPI - Prometheus.json" \ "UniFi-Poller_ Client Insights - Prometheus.json" \ "UniFi-Poller_ Network Sites - Prometheus.json" \ "UniFi-Poller_ UAP Insights - Prometheus.json" \