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