From 3550c6af2ab976da40de461394a40b678f17e3c4 Mon Sep 17 00:00:00 2001 From: Ryan Yin Date: Fri, 24 Jul 2026 12:42:14 +0800 Subject: [PATCH] feat(monitoring): collect homelab UPS metrics --- .../monitoring/victoriametrics.nix | 16 +++++ hosts/k8s/kubevirt-shushou/ups.nix | 51 +++++++++++++++ .../tests/ups-metrics/expected.nix | 44 +++++++++++++ .../x86_64-linux/tests/ups-metrics/expr.nix | 63 +++++++++++++++++++ 4 files changed, 174 insertions(+) create mode 100644 hosts/k8s/kubevirt-shushou/ups.nix create mode 100644 outputs/x86_64-linux/tests/ups-metrics/expected.nix create mode 100644 outputs/x86_64-linux/tests/ups-metrics/expr.nix diff --git a/hosts/idols-aquamarine/monitoring/victoriametrics.nix b/hosts/idols-aquamarine/monitoring/victoriametrics.nix index a56ac3b2..d7913dfe 100644 --- a/hosts/idols-aquamarine/monitoring/victoriametrics.nix +++ b/hosts/idols-aquamarine/monitoring/victoriametrics.nix @@ -71,6 +71,22 @@ } ]; } + { + job_name = "nut-exporter-homelab-ups"; + scrape_interval = "30s"; + metrics_path = "/ups_metrics"; + params.ups = [ "homelab" ]; + static_configs = [ + { + targets = [ "${myvars.networking.hostsAddr.kubevirt-shushou.ipv4}:9199" ]; + labels.type = "app"; + labels.app = "nut"; + labels.host = "kubevirt-shushou"; + labels.env = "homelab"; + labels.cluster = "homelab"; + } + ]; + } { job_name = "postgres-exporter"; scrape_interval = "30s"; diff --git a/hosts/k8s/kubevirt-shushou/ups.nix b/hosts/k8s/kubevirt-shushou/ups.nix new file mode 100644 index 00000000..a79c0e60 --- /dev/null +++ b/hosts/k8s/kubevirt-shushou/ups.nix @@ -0,0 +1,51 @@ +{ myvars, ... }: +let + hostAddress = myvars.networking.hostsAddr.kubevirt-shushou.ipv4; +in +{ + power.ups = { + enable = true; + mode = "standalone"; + + # Metrics only. Coordinated shutdown of all homelab hosts is a separate feature. + upsmon = { + enable = false; + settings.POWERDOWNFLAG = null; + }; + + upsd.listen = [ + { + address = "127.0.0.1"; + port = 3493; + } + ]; + + ups.homelab = { + driver = "nutdrv_qx"; + port = "auto"; + description = "Homelab UPS"; + directives = [ + "vendorid = 0665" + "productid = 5161" + ]; + }; + }; + + services.prometheus.exporters.nut = { + enable = true; + listenAddress = hostAddress; + port = 9199; + nutServer = "127.0.0.1"; + + # The NixOS module omits --nut.vars_enable when nutVariables is empty, but the exporter needs + # an explicitly empty value to export every numeric NUT variable. + extraFlags = [ "--nut.vars_enable=" ]; + }; + + systemd.services.prometheus-nut-exporter = { + after = [ + "upsd.service" + "upsdrv.service" + ]; + }; +} diff --git a/outputs/x86_64-linux/tests/ups-metrics/expected.nix b/outputs/x86_64-linux/tests/ups-metrics/expected.nix new file mode 100644 index 00000000..0bec1357 --- /dev/null +++ b/outputs/x86_64-linux/tests/ups-metrics/expected.nix @@ -0,0 +1,44 @@ +{ + nut = { + enable = true; + mode = "standalone"; + upsmonEnable = false; + killPowerEnable = false; + upsdListen = [ + { + address = "127.0.0.1"; + port = 3493; + } + ]; + driver = "nutdrv_qx"; + port = "auto"; + vendorId = true; + productId = true; + }; + + exporter = { + enable = true; + listenAddress = "192.168.5.182"; + port = 9199; + nutServer = "127.0.0.1"; + exportsAllNumericVariables = true; + startsAfterNut = true; + requiresNut = false; + }; + + victoriametricsJobCount = 1; + victoriametrics = { + job_name = "nut-exporter-homelab-ups"; + scrape_interval = "30s"; + metrics_path = "/ups_metrics"; + params.ups = [ "homelab" ]; + target = "192.168.5.182:9199"; + labels = { + type = "app"; + app = "nut"; + host = "kubevirt-shushou"; + env = "homelab"; + cluster = "homelab"; + }; + }; +} diff --git a/outputs/x86_64-linux/tests/ups-metrics/expr.nix b/outputs/x86_64-linux/tests/ups-metrics/expr.nix new file mode 100644 index 00000000..0decca95 --- /dev/null +++ b/outputs/x86_64-linux/tests/ups-metrics/expr.nix @@ -0,0 +1,63 @@ +{ + lib, + outputs, + ... +}: +let + shushou = outputs.nixosConfigurations.kubevirt-shushou.config; + aquamarine = outputs.nixosConfigurations.aquamarine.config; + ups = shushou.power.ups.ups.homelab or { }; + exporter = shushou.services.prometheus.exporters.nut; + exporterService = + shushou.systemd.services.prometheus-nut-exporter or { + after = [ ]; + requires = [ ]; + }; + scrapeJobs = builtins.filter ( + job: (job.job_name or "") == "nut-exporter-homelab-ups" + ) aquamarine.services.victoriametrics.prometheusConfig.scrape_configs; + scrapeJob = if builtins.length scrapeJobs == 1 then builtins.head scrapeJobs else null; +in +{ + nut = { + enable = shushou.power.ups.enable; + mode = shushou.power.ups.mode; + upsmonEnable = shushou.power.ups.upsmon.enable; + killPowerEnable = shushou.systemd.services.ups-killpower.enable or false; + upsdListen = shushou.power.ups.upsd.listen; + driver = ups.driver or null; + port = ups.port or null; + vendorId = builtins.elem "vendorid = 0665" (ups.directives or [ ]); + productId = builtins.elem "productid = 5161" (ups.directives or [ ]); + }; + + exporter = { + enable = exporter.enable; + listenAddress = exporter.listenAddress; + port = exporter.port; + nutServer = exporter.nutServer; + exportsAllNumericVariables = builtins.elem "--nut.vars_enable=" exporter.extraFlags; + startsAfterNut = + builtins.elem "upsd.service" exporterService.after + && builtins.elem "upsdrv.service" exporterService.after; + requiresNut = + builtins.elem "upsd.service" exporterService.requires + && builtins.elem "upsdrv.service" exporterService.requires; + }; + + victoriametricsJobCount = builtins.length scrapeJobs; + victoriametrics = + if scrapeJob == null then + null + else + { + inherit (scrapeJob) + job_name + scrape_interval + metrics_path + params + ; + target = builtins.head (builtins.head scrapeJob.static_configs).targets; + labels = (builtins.head scrapeJob.static_configs).labels; + }; +}