Manual

Run upsmon as a NUT secondary on Fedora and Fedora Atomic

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 upsmon yet; 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 DEADTIME while 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, with secondary rights.
  • 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 (nc is in the nmap-ncat package). 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 Goodbye
    

    Expected: your <UPS> appears between BEGIN LIST UPS and END LIST UPS.

  • Fedora Atomic only: check free space with df -h /sysroot and look for a staged deployment with rpm-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.conf and upsmon.conf to *.orig once. Undo restores from them.

Steps#

  1. Install nut-client.

    Fedora:

    dnf install -y nut-client
    

    Output 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 the nut user and group are created.

    Fedora Atomic:

    rpm-ostree install --idempotent --apply-live -y nut-client
    

    Output 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 override
    

    Expected: 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, check rpm -q nut-client before 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-client
    

    That is the output of uptime -s; rpm -q nut-client; rpm-ostree status --booted | head -6 (the head cut drops the later lines, such as LocalPackages). Expected: nut-client in LayeredPackages and rpm -q nut-client prints a version.

  2. 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/*.orig
    

    Output on Fedora 44:

    /etc/ups/nut.conf.orig
    /etc/ups/upsmon.conf.orig
    

    Expected: both .orig files exist. Running the loop again does not overwrite them.

  3. 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_PASS
    

    Type <NUT_PASSWORD> and press Enter. No output.

  4. Set NUT to client mode.

    printf 'MODE=netclient\n' > /etc/ups/nut.conf
    

    No output. netclient means this host runs only upsmon and no local driver or server.

  5. Write upsmon.conf as a secondary and make it readable by the nut group only.

    The password goes into the MONITOR line unquoted, so use one without spaces or #. The umask 027 stays 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.conf
    

    The 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.conf
    

    Expected: root:nut 640. The file holds the password.

    What the lines do: 1 is the number of power supplies this UPS feeds on the host; secondary means this host shuts itself down and leaves powering off the UPS to the primary. DEADTIME 300 lets the server go missing for 5 minutes before upsmon declares the UPS dead (the default is 15 s); why I picked 300 for my server is in part 5 of the UPS series. FINALDELAY 5 is the wait between the shutdown notice and SHUTDOWNCMD.

  6. Check the login without arming a shutdown. This runs upsmon in the foreground for 15 s from a temporary copy of the config whose SHUTDOWNCMD is /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: upsc prints the UPS status (OL = on line) and upsmon prints Logged into UPS <UPS>@<SERVER>. Fedora 44 Server printed the same three lines (timestamp 0.023528). If the login line is missing, stop here: nothing is enabled yet.

  7. Enable nut.target and nut-monitor, then start it. nut-monitor.service is WantedBy=nut.target only, 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.service
    

    Output 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.target symlink is created (on Fedora 44 only that first line appeared: nut-monitor was already enabled by an earlier run of my script).

  8. 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 cat
    

    Output 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 monitoring
    

    Expected: active, enabled, and the UPS status. The writepid and upsnotify lines 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)CauseFix
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 nowNot 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-monitor active at 12:17:08 EEST, nut.target and nut-monitor both enabled, upsc reads OL. The boot journal for the unit holds the same writepid and upsnotify lines as after the manual restart, plus a notice that no POWERDOWNFLAG is 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: /sysroot was 99 % full. rpm-ostree status showed a deployment for 44.20261012.1 already staged above the booted 44.20260921, with Diff: 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 with packages would be removed: 13. I rebooted instead of overriding. The box came up at 11:56 (its local time) on 44.20261012.1 with nut-client in LayeredPackages. 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 UPS in the foreground run, service left disabled on purpose. The dnf install also printed Symlinking /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-client had been installed by an earlier run on 2026-10-15 (22:11 UTC), which had also enabled nut-monitor, so this run's systemctl enable created only the nut.target symlink. Fedora 45 installed and enabled at 06:51 UTC. Both read OL and logged the same writepid/upsnotify warnings. 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. /etc is 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 writepid warning is on my list to check before the shutdown drill, together with whether SELinux lets SHUTDOWNCMD run.

Filed under: ups, nut, fedora, linux, systemd, power

Sources#

  • upsmon.conf(5): MONITOR syntax (<upsname>@<hostname>, power value, secondary), DEADTIME (default 15 s), FINALDELAY, NOCOMMWARNTIME (default 300 s).
  • upsmon(8): -D/-F foreground 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.