Run upsmon as a NUT secondary on Fedora and Fedora Atomic
Contents
This installs upsmon on a Fedora or Fedora Atomic host as a read-only secondary of a remote NUT server, checks the login before anything is armed, and enables it so it starts again after a reboot.
- Tested on
- Fedora 44 KDE Plasma Desktop and Fedora 45 Prerelease (installed, enabled, status read; reboot checked on Fedora 44); Fedora 44 Server and Bazzite 44.20261012.1 (installed, configured, login checked, service left disabled). nut-client / upsmon 2.8.5 (2026-10-16, re-checked 2026-10-17). The server side was my own NUT server (nut-observer), not
upsd; the steps are the stock client side. - Not verified
- the shutdown itself. No host has been powered down by
upsmonyet; this manual proves login, status and start-at-boot only. - Needs
- root on the host; a NUT server reachable on TCP 3493 with a UPS name and a user/password for this host; one reboot on Fedora Atomic if a deployment is already staged.
- Time
- 15 min (plus one reboot on Fedora Atomic).
- Risk
- once enabled, the host powers itself off when the server reports the UPS on battery and low battery, or when the server stays unreachable for
DEADTIMEwhile the UPS was last seen on battery. Reversible: Undo below restores the original configs.
Before you start#
Placeholders, used in every step below:
<UPS>: the UPS name on the NUT server.<SERVER>: the NUT server's address.<NUT_USER>,<NUT_PASSWORD>: the login the server holds for this host, withsecondaryrights.
Open a root shell (
sudo -i) and keep it for all steps: the variables set in step 3 are used later.Check that the server answers and lists your UPS (
ncis in thenmap-ncatpackage). Output from the Fedora 44 box, UPS names replaced, cut marked:$ printf 'LIST UPS\nLOGOUT\n' | nc <SERVER> 3493 BEGIN LIST UPS … UPS <UPS> "BLUETTI AC180, tier t2, via nut-observer" … END LIST UPS OK GoodbyeExpected: your
<UPS>appears betweenBEGIN LIST UPSandEND LIST UPS.Fedora Atomic only: check free space with
df -h /sysrootand look for a staged deployment withrpm-ostree status. Lines from both on the Bazzite box before the install, cut marked:/dev/nvme0n1p3 930G 914G 15G 99% /sysroot … State: idle Deployments: ostree-image-signed:docker://ghcr.io/ublue-os/bazzite-nvidia-open:stable … Version: 44.20261012.1 (2026-10-13T00:06:38Z) … Diff: 406 upgraded, 13 removed, 54 added … ● ostree-image-signed:docker://ghcr.io/ublue-os/bazzite-nvidia-open:stable … Version: 44.20260921 (2026-09-21T05:59:02Z)Expected: free space above rpm-ostree's 3 % floor (this box was at 99 % used, Failure modes row 1). A deployment listed above the booted one (
●) is already staged; that is the state in which my live apply was refused (row 2).Backups: step 2 copies the existing
nut.confandupsmon.confto*.origonce. Undo restores from them.
Steps#
Install nut-client.
Fedora:
dnf install -y nut-clientOutput on Fedora 45, cut marked:
… [1/1] Total 100% | 1.1 MiB/s | 423.4 KiB | 00m00s Running transaction [1/3] Verify package files 100% | 500.0 B/s | 1.0 B | 00m00s [2/3] Prepare transaction 100% | 2.0 B/s | 1.0 B | 00m00s >>> Running sysusers scriptlet: nut-client-0:2.8.5-9.fc45.x86_64 >>> Finished sysusers scriptlet: nut-client-0:2.8.5-9.fc45.x86_64 >>> Scriptlet output: >>> Creating group 'nut' with GID 57. >>> Creating user 'nut' (Network UPS Tools) with UID 57 and GID 57. >>> [3/3] Installing nut-client-0:2.8.5-9.f 100% | 546.2 KiB/s | 1.2 MiB | 00m02s Complete!Expected:
Complete!. On a first install thenutuser and group are created.Fedora Atomic:
rpm-ostree install --idempotent --apply-live -y nut-clientOutput on Bazzite (with a deployment already staged, see Before you start), cut marked:
… Writing OSTree commit...done Staging deployment...done Freed: 140.4 MB (pkgcache branches: 0) error: packages would be removed: 13, allow replacement to overrideExpected: the package is applied live. I have not seen that path: on my box the live apply was refused (Failure modes row 2), the package went into the staged deployment, and a reboot brought it in.
If the live apply is refused like this (Failure modes row 2), run
systemctl reboot; if the install failed on disk space (row 1), nothing was staged: free space and run the install again. Either way, checkrpm -q nut-clientbefore step 2. Without the package on the booted deployment, steps 2–5 have nothing to configure. After the reboot:2026-10-16 11:56:43 nut-client-2.8.5-8.fc44.x86_64 State: idle BootedDeployment: ● ostree-image-signed:docker://ghcr.io/ublue-os/bazzite-nvidia-open:stable … Version: 44.20261012.1 (2026-10-13T00:06:38Z) LayeredPackages: coolercontrol lact liquidctl nut-clientThat is the output of
uptime -s; rpm -q nut-client; rpm-ostree status --booted | head -6(theheadcut drops the later lines, such asLocalPackages). Expected:nut-clientinLayeredPackagesandrpm -q nut-clientprints a version.Back up the existing configs once.
for f in nut.conf upsmon.conf; do [ -f /etc/ups/$f ] && [ ! -f /etc/ups/$f.orig ] && cp -a /etc/ups/$f /etc/ups/$f.orig done ls /etc/ups/*.origOutput on Fedora 44:
/etc/ups/nut.conf.orig /etc/ups/upsmon.conf.origExpected: both
.origfiles exist. Running the loop again does not overwrite them.Set the placeholders and read the password without putting it in argv or shell history.
UPS=<UPS> SERVER=<SERVER> NUT_USER=<NUT_USER> read -rs NUT_PASSType
<NUT_PASSWORD>and press Enter. No output.Set NUT to client mode.
printf 'MODE=netclient\n' > /etc/ups/nut.confNo output.
netclientmeans this host runs onlyupsmonand no local driver or server.Write upsmon.conf as a secondary and make it readable by the nut group only.
The password goes into the
MONITORline unquoted, so use one without spaces or#. Theumask 027stays in effect for the rest of this root shell.umask 027 cat > /etc/ups/upsmon.conf <<EOF MONITOR $UPS@$SERVER 1 $NUT_USER $NUT_PASS secondary MINSUPPLIES 1 SHUTDOWNCMD "/usr/sbin/shutdown -h +0" POLLFREQ 5 POLLFREQALERT 5 DEADTIME 300 NOCOMMWARNTIME 300 FINALDELAY 5 EOF chown root:nut /etc/ups/upsmon.conf chmod 640 /etc/ups/upsmon.conf stat -c '%U:%G %a %n' /etc/ups/upsmon.confThe heredoc delimiter is unquoted on purpose, so the shell fills in the variables from step 3. Output on Fedora 44:
root:nut 640 /etc/ups/upsmon.confExpected:
root:nut 640. The file holds the password.What the lines do:
1is the number of power supplies this UPS feeds on the host;secondarymeans this host shuts itself down and leaves powering off the UPS to the primary.DEADTIME 300lets the server go missing for 5 minutes beforeupsmondeclares the UPS dead (the default is 15 s); why I picked 300 for my server is in part 5 of the UPS series.FINALDELAY 5is the wait between the shutdown notice andSHUTDOWNCMD.Check the login without arming a shutdown. This runs
upsmonin the foreground for 15 s from a temporary copy of the config whoseSHUTDOWNCMDis/bin/true.T=$(mktemp -d /run/upsmon-verify.XXXXXX); chmod 750 "$T"; chgrp nut "$T" sed 's#^SHUTDOWNCMD .*#SHUTDOWNCMD "/bin/true"#' /etc/ups/upsmon.conf > "$T/upsmon.conf" cp /etc/ups/nut.conf "$T/"; chgrp nut "$T"/*; chmod 640 "$T"/* systemctl is-enabled nut-monitor.service nut.target 2>/dev/null | paste -sd' ' || true upsc "$UPS@$SERVER" ups.status NUT_CONFPATH=$T NUT_STATEPATH=$T timeout 15 upsmon -D -F 2>&1 | grep -E 'Logged into|Login|denied|ERR|failed to connect' || true rm -rf "$T"Output on Bazzite, names replaced, cut marked:
… disabled disabled OL 0.013950 [D1:main-loop] Logged into UPS <UPS>@<SERVER>Expected:
upscprints the UPS status (OL= on line) andupsmonprintsLogged into UPS <UPS>@<SERVER>. Fedora 44 Server printed the same three lines (timestamp0.023528). If the login line is missing, stop here: nothing is enabled yet.Enable nut.target and nut-monitor, then start it.
nut-monitor.serviceisWantedBy=nut.targetonly, so enabling the service alone does not start it at boot (part 5 has the story).systemctl enable nut.target nut-monitor.service systemctl restart nut-monitor.serviceOutput on Fedora 45:
Created symlink '/etc/systemd/system/multi-user.target.wants/nut.target' → '/usr/lib/systemd/system/nut.target'. Created symlink '/etc/systemd/system/upsmon.service' → '/usr/lib/systemd/system/nut-monitor.service'. Created symlink '/etc/systemd/system/nut.target.wants/nut-monitor.service' → '/usr/lib/systemd/system/nut-monitor.service'.Expected: the
multi-user.target.wants/nut.targetsymlink is created (on Fedora 44 only that first line appeared:nut-monitorwas already enabled by an earlier run of my script).Read the service state, the UPS status and the last log lines.
sleep 5 systemctl is-active nut-monitor.service; systemctl is-enabled nut.target upsc "$UPS@$SERVER" ups.status journalctl -u nut-monitor.service -n 6 --no-pager -o catOutput on Fedora 45, cut marked (Fedora 44 printed the same lines):
… active enabled OL Warning: no custom notification command defined, just so you know writepid: fopen /run/nut/upsmon.pid: Permission denied upsnotify: notify about state NOTIFY_STATE_READY_WITH_PID with libsystemd: was requested, but not running as a service unit now, will not spam more about it upsnotify: failed to notify about state NOTIFY_STATE_READY_WITH_PID: no notification tech defined, will not spam more about it upsmon: initialized OS integration for sleep inhibitor upsmon: initialized OS integration for sleep/wake monitoringExpected:
active,enabled, and the UPS status. Thewritepidandupsnotifylines appeared on both enabled hosts; the service stayed active and the status read worked. I have not determined their cause (Failure modes row 3).
Verify#
Reboot the host, then check that nut-monitor came up with the boot and that both units are enabled. Output on Fedora 44, rebooted after step 8:
$ who -b
system boot 2026-10-16 12:17
$ systemctl show nut-monitor -p ActiveEnterTimestamp
ActiveEnterTimestamp=Fri 2026-10-16 12:17:08 EEST
$ systemctl is-enabled nut.target nut-monitor
enabled
enabled
Expected: ActiveEnterTimestamp falls in the same minute as the boot, and both units print enabled. This proves start-at-boot only. It does not prove the host shuts down on low battery: that drill has not been run.
Undo / rollback#
Stop and disable upsmon, then restore the original configs (from my rollback script):
systemctl disable --now nut-monitor.service || true
for f in nut.conf upsmon.conf; do
[ -f /etc/ups/$f.orig ] && cp -a /etc/ups/$f.orig /etc/ups/$f
done
The package stays installed, nut.target stays enabled (systemctl disable nut.target turns it off too) and the *.orig files stay. To remove the package as well: dnf remove nut-client, or rpm-ostree uninstall nut-client on Fedora Atomic, which takes effect after a reboot. [UNVERIFIED: package removal not run]
[UNVERIFIED: rollback not exercised on any host]
Failure modes#
| Symptom (exact message) | Cause | Fix |
|---|---|---|
1. error: importing RPMs: Importing package 'nut-client': Importing archive: ostree-tar: Failed to handle file: ostree-tar: Failed to import file: Writing content object: min-free-space-percent '3%' would be exceeded, at least 12.3 kB requested | /sysroot was 99 % full (15G free of 930G). | Free space. With 190G free the same install got past the import. |
2. error: packages would be removed: 13, allow replacement to override (after Staging deployment...done) | A deployment (44.20261012.1) was already staged, and its diff listed 13 removed. The counts match; I did not reproduce the error without the staged update. [UNVERIFIED: cause] | Reboot into the staged deployment. nut-client was layered in it after the reboot, with no second install run. |
3. writepid: fopen /run/nut/upsmon.pid: Permission denied, plus upsnotify: … not running as a service unit now | Not determined. Seen on both enabled hosts, after systemctl restart and again in the boot journal. | None applied. The service stays active and the status reads OL. Whether it affects a real shutdown is untested. |
Field notes#
- 2026-10-17, Fedora 44 KDE Plasma Desktop, nut-client 2.8.5-8.fc44: checked after the 2026-10-16 reboot. Boot at 12:17,
nut-monitoractive at 12:17:08 EEST,nut.targetandnut-monitorboth enabled,upscreadsOL. The boot journal for the unit holds the samewritepidandupsnotifylines as after the manual restart, plus a notice that noPOWERDOWNFLAGis configured, which the same notice says matters little on a host that is not primary for any device. Same day, Fedora 45 Prerelease (nut-client 2.8.5-9.fc45): still enabled and active; that box has not rebooted since the install (last boot 2026-10-14), so start-at-boot is proven on Fedora 44 only. - 2026-10-16, Bazzite 44.20261012.1 (rpm-ostree, booted 44.20260921 at the start): the first install at 06:53 UTC stopped on the 3 % free-space floor:
/sysrootwas 99 % full.rpm-ostree statusshowed a deployment for 44.20261012.1 already staged above the booted 44.20260921, withDiff: 406 upgraded, 13 removed, 54 added. After I freed space (190G free), the install at 07:40 UTC imported the package, staged a new deployment, and then refused the live apply withpackages would be removed: 13. I rebooted instead of overriding. The box came up at 11:56 (its local time) on 44.20261012.1 withnut-clientinLayeredPackages. The login check (step 6) passed at 09:27 UTC; the service was left disabled on purpose. - 2026-10-16, Fedora 44 Server (Fedora Linux 44 Server Edition), nut-client 2.8.5-8.fc44: install, configs and the login check (step 6) at 06:52 UTC;
Logged into UPSin the foreground run, service left disabled on purpose. The dnf install also printedSymlinking /usr/sbin/upsmon -> /usr/bin/upsmon. - 2026-10-16, Fedora 44 KDE Plasma Desktop and Fedora 45 Prerelease: enable runs at 06:50 and 06:51 UTC. On Fedora 44,
nut-clienthad been installed by an earlier run on 2026-10-15 (22:11 UTC), which had also enablednut-monitor, so this run'ssystemctl enablecreated only thenut.targetsymlink. Fedora 45 installed and enabled at 06:51 UTC. Both readOLand logged the samewritepid/upsnotifywarnings. All four hosts were set up with my install script, which has two modes: enable runs steps 2, 4, 5, 7 and 8 (no login check), verify runs steps 2, 4, 5 and 6 and stops before enabling. The desktops ran enable, the server and Bazzite ran verify, so no host ran step 6 directly followed by step 7.
Notes#
- Why secondary: my NUT server is read-only and each host watches one of its UPS names as a client. The design is in the observer design post.
- Fedora Atomic, enable path: steps 7 and 8 ran on Fedora only.
/etcis writable on Bazzite and the commands are the same, but I have not enabled the service there.[UNVERIFIED: steps 7–8 on Fedora Atomic] - Live apply on Atomic: a live apply on a box with no staged deployment was not tried.
[UNVERIFIED] - The
writepidwarning is on my list to check before the shutdown drill, together with whether SELinux letsSHUTDOWNCMDrun.
Filed under: ups, nut, fedora, linux, systemd, power
Sources#
- upsmon.conf(5):
MONITORsyntax (<upsname>@<hostname>, power value,secondary),DEADTIME(default 15 s),FINALDELAY,NOCOMMWARNTIME(default 300 s). - upsmon(8):
-D/-Fforeground debug run,NUT_CONFPATH, dead-UPS handling while on battery. - nut.conf(5):
MODE=netclient. - Architecture of apply-live: apply-live creates a pending deployment first and computes a package-level diff to tell additions from upgrades.
- nut-observer README: the NUT server these hosts monitor.