nut-ups-prometheus¶
Modules
A drop-in NixOS module that wires a locally-attached UPS into
NUT (Network UPS Tools) and the Prometheus
NUT exporter, with everything bound to 127.0.0.1. It captures two traps
that bite people the first time they do this, and turns them into options.
What problem it solves¶
You have a UPS plugged into a machine over USB and you want:
- NUT to talk to it (
upsc ups@localhostreturns live telemetry), and - Prometheus to scrape battery charge / runtime, input & output voltage,
load, temperature,
ups.status, etc.
The exporter and the NUT server both listen only on loopback, so nothing is exposed on the network. Remote scraping goes over a VPN or a reverse proxy of your choosing — the module makes no assumptions about that.
The two traps (why this exists)¶
1. nutdrv_qx misidentifies the device under port = "auto"¶
usbhid-ups (the default) autodetects most APC Back-UPS / Smart-UPS USB
models fine. But cheap and OEM "megatec"-protocol units driven by nutdrv_qx
are frequently misidentified when the port is left on auto — the driver
grabs the wrong USB device or fails to match at all.
The fix is an explicit USB match. Run lsusb, find the xxxx:yyyy for the
UPS, and set:
services.nut-ups-prometheus = {
driver = "nutdrv_qx";
vendorid = "0001"; # the part before the colon in lsusb
productid = "0000"; # the part after
};
2. Auto-shutdown, deliberately disabled¶
By default NUT will power the host down when the battery reaches critical. That is often not what you want: for machines that should ride the battery all the way out, an automated shutdown is worse than the outage.
This module defaults to disableAutoShutdown = true, which sets
MINSUPPLIES = 0 and replaces SHUTDOWNCMD with a command that only writes a
log line. A critical-battery event is recorded but never triggers a shutdown.
Set disableAutoShutdown = false to get the normal, shut-the-host-down
behaviour.
Usage¶
{
imports = [ ./modules/nut-ups-prometheus ];
services.nut-ups-prometheus = {
enable = true;
# A file containing the NUT monitor-user password. Provide it via any
# secrets mechanism (plain file, sops-nix, agenix, …). Must be readable
# by the NUT daemons.
passwordFile = "/run/secrets/nut-ups-password";
# APC USB models usually just work with the defaults:
# driver = "usbhid-ups"; port = "auto";
#
# For an OEM/megatec unit, pin the driver + USB ids instead:
# driver = "nutdrv_qx";
# vendorid = "0001";
# productid = "0000";
};
}
Then confirm with upsc ups@localhost and scrape
http://127.0.0.1:9199/ups_metrics from Prometheus.
Options¶
| Option | Default | Purpose |
|---|---|---|
enable |
false |
Turn the module on. |
passwordFile |
(required) | Path to the file holding the monitor user's password. |
upsName |
"ups" |
NUT instance name; addressed as <name>@localhost. |
driver |
"usbhid-ups" |
NUT driver. Use nutdrv_qx for OEM/megatec units. |
port |
"auto" |
Driver port. auto = USB autodetect. |
vendorid / productid |
null |
Explicit USB match — required when auto misidentifies the device. |
monUser |
"monuser" |
Name of the upsmon monitor user. |
disableAutoShutdown |
true |
Ride the battery out instead of shutting down on critical battery. |
description |
"Local UPS" |
Free-text description in ups.conf. |
extraConfig |
"" |
Lines appended verbatim to the UPS's ups.conf section. |
exporterPort |
9199 |
prometheus-nut-exporter port (bound to 127.0.0.1). |
Caveats¶
- Localhost only. The exporter is bound to
127.0.0.1explicitly.upsdgets noLISTENdirective from this module, which leaves it on NUT's own default of loopback — if you addpower.ups.upsd.listenentries yourself you are widening that. Remote Prometheus scraping must go over a VPN or reverse proxy you set up yourself. passwordFilepermissions. The file must be readable by the NUT service accounts. Mode0440with owner/group matching the NUT services is a safe default.extraConfigis an escape hatch. Driver settings not covered by the named options (polling intervals, battery voltage overrides, …) go there and are appended verbatim to the UPS'sups.confsection.
Source¶
modules/nut-ups-prometheus/default.nix
# nut-ups-prometheus
#
# Wire a locally-attached UPS into NUT (Network UPS Tools) plus the Prometheus
# NUT exporter, everything bound to localhost. Two real-world traps are baked in
# as options:
#
# 1. Cheap / OEM "megatec"-style UPSes driven by `nutdrv_qx` are frequently
# misidentified under `port = "auto"`, so they need an explicit
# `vendorid`/`productid` USB match.
# 2. Auto-shutdown can be deliberately disabled (`MINSUPPLIES = 0` plus a
# log-only `SHUTDOWNCMD`) so a critical-battery event rides the battery out
# instead of powering the host down.
#
# Drop-in NixOS module. Import it and set at minimum `enable` and
# `passwordFile`.
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.nut-ups-prometheus;
in
{
options.services.nut-ups-prometheus = {
enable = mkEnableOption "NUT-managed UPS + Prometheus exporter (localhost-only)";
upsName = mkOption {
type = types.str;
default = "ups";
description = "NUT UPS instance name. Used as `<name>@localhost` in upsc / dashboards.";
};
driver = mkOption {
type = types.str;
default = "usbhid-ups";
description = ''
NUT driver. `usbhid-ups` autodetects most APC Back-UPS / Smart-UPS USB
models. Cheap / OEM "megatec"-protocol units typically need
`nutdrv_qx` together with an explicit `vendorid`/`productid`.
'';
};
port = mkOption {
type = types.str;
default = "auto";
description = "NUT driver port (`auto` = USB autodetect).";
};
vendorid = mkOption {
type = types.nullOr types.str;
default = null;
example = "0001";
description = ''
Optional USB vendor ID. Drivers such as `nutdrv_qx` often misidentify
the device under `port = "auto"`, so pin the exact USB match here. Find
it with `lsusb` (the `xxxx:yyyy` before the colon is the vendor ID).
'';
};
productid = mkOption {
type = types.nullOr types.str;
default = null;
example = "0000";
description = "Optional USB product ID, paired with `vendorid`.";
};
description = mkOption {
type = types.str;
default = "Local UPS";
description = "Human-readable description written to the UPS's `ups.conf` section.";
};
monUser = mkOption {
type = types.str;
default = "monuser";
description = "NUT monitor user (the upsmon client that reads UPS state).";
};
passwordFile = mkOption {
type = types.path;
example = "/run/secrets/nut-ups-password";
description = ''
Path to a file containing the password for `monUser`. Provide it via
whatever secrets mechanism you use (plain file, sops-nix, agenix, …).
The file must be readable by the NUT daemons (mode 0440, owner/group
matching the NUT service is a safe choice).
'';
};
disableAutoShutdown = mkOption {
type = types.bool;
default = true;
description = ''
When true (the default), a critical-battery event does NOT power the
host down: `MINSUPPLIES = 0` and `SHUTDOWNCMD` only logs. The machine
rides the battery out. Set to false for the normal NUT behaviour of
shutting the host down when the battery hits critical.
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = ''
Extra lines appended verbatim to this UPS's section in `ups.conf` —
escape hatch for driver-specific settings not covered by the named
options above.
'';
};
exporterPort = mkOption {
type = types.port;
default = 9199;
description = "prometheus-nut-exporter listen port (bound to 127.0.0.1).";
};
};
config = mkIf cfg.enable {
power.ups = {
enable = true;
mode = "standalone";
ups.${cfg.upsName} = {
inherit (cfg) driver port description;
directives =
lib.optional (cfg.vendorid != null) ''vendorid = "${cfg.vendorid}"''
++ lib.optional (cfg.productid != null) ''productid = "${cfg.productid}"''
++ lib.optional (cfg.extraConfig != "") cfg.extraConfig;
};
users.${cfg.monUser} = {
upsmon = "primary";
passwordFile = cfg.passwordFile;
};
upsmon = {
monitor.${cfg.upsName} = {
system = "${cfg.upsName}@localhost";
user = cfg.monUser;
passwordFile = cfg.passwordFile;
type = "primary";
powerValue = 0;
};
settings = mkIf cfg.disableAutoShutdown {
# Ride the battery out: never trigger an automated shutdown.
MINSUPPLIES = 0;
SHUTDOWNCMD = ''"${pkgs.util-linux}/bin/logger -t upsmon ALERT: UPS reports critical battery; auto-shutdown intentionally disabled"'';
};
};
};
# Silence the "no discharge estimate" init warning some drivers emit.
systemd.services.upsdrv.environment.NUT_QUIET_INIT_NDE_WARNING = "true";
services.prometheus.exporters.nut = {
enable = true;
listenAddress = "127.0.0.1";
port = cfg.exporterPort;
nutServer = "127.0.0.1";
nutVariables = [
"battery.charge"
"battery.runtime"
"battery.voltage"
"battery.voltage.high"
"battery.voltage.low"
"battery.voltage.nominal"
"input.voltage"
"input.voltage.nominal"
"input.frequency"
"input.frequency.nominal"
"output.voltage"
"output.voltage.nominal"
"output.current"
"output.current.nominal"
"output.frequency"
"output.frequency.nominal"
"output.powerfactor"
"ups.load"
"ups.power.nominal"
"ups.temperature"
"ups.status"
];
};
};
}