Thanks to the Fedora developers, nut does actually ship some unit files for systemd. Unfortunately, those unit files provide a somewhat minimal functionality, with fairly fixed startup order and without taking advantage of systemd’s unit activation features. This caused some problems when I was building my new server a while back. This new machine is quite wicked fast with a quad-core IVB and an SSD—so fast, in fact, that systemd was starting nut’s UPS driver before the UPS itself was probed on the USB bus. As a result, startup failed quite badly, and it didn’t automatically recover.
So, obviously, we have to delay the startup of the nut driver until the USB device has been probed. But what about the other services? As it turns out, nut-server(upsd) and nut-monitor(upsmon) are actually perfectly happy to start up in any order with no more than a few warning messages in the logs as they wait for the other services to become available. So lets simplify the unit files for those. First, /etc/systemd/system/nut-server.service
:
[Unit]
Description=Network UPS Tools - power devices information server
[Service]
ExecStart=/usr/sbin/upsd
Type=forking
[Install]
WantedBy=multi-user.target
If you have configured upsd to listen to a particular network interface or on a particular network address (in upsd.conf) it almost certainly needs the interface to be configured first; you should add in After=network.target
.
Now, we can tweak the unit file for the monitoring process: /etc/systemd/system/nut-monitor.service
:
[Unit]
Description=Network UPS Tools - power device monitor and shutdown controller
After=nut-server.service
[Service]
ExecStart=/usr/sbin/upsmon
PIDFile=/run/nut/upsmon.pid
Type=forking
[Install]
WantedBy=multi-user.target
I’ve left in the After=nut-server.service
for only one reason: To reduce the warning messages in the log output when starting both upsd and upsmon on the same system. It’s not strictly required.
One thing to double-check: Ensure that the nut packages on your system have installed the file /usr/lib/systemd/system-shutdown/nutshutdown
. This file doesn’t need any modifications, but it’s required to handle emergency poweroffs and test reboots correctly. It should look something like this, and it must be executable:
#!/bin/sh
/usr/sbin/upsmon -K >/dev/null 2>&1 && /usr/sbin/upsdrvctl shutdown
Now for the fun bit; handling starting up the nut driver for the USB UPS automatically once it’s been probed by the kernel. Ideally, we want to make this independent of things like which USB port it happens to be plugged into—this means creating a device file symlink with an appropriate stable name. Lets see what we have available…
# udevadm info --attribute-walk --name /dev/bus/usb/003/003
looking at device '/devices/pci0000:00/0000:00:1a.0/usb3/3-1/3-1.1':
KERNEL=="3-1.1"
SUBSYSTEM=="usb"
DRIVER=="usb"
[...]
ATTR{idVendor}=="051d"
ATTR{quirks}=="0x0"
ATTR{serial}=="4B1111P37995 "
ATTR{version}==" 1.10"
ATTR{urbnum}=="13527"
ATTR{ltm_capable}=="no"
ATTR{manufacturer}=="APC"
ATTR{removable}=="removable"
ATTR{idProduct}=="0002"
ATTR{bDeviceClass}=="00"
ATTR{product}=="Back-UPS ES 550 FW:843.K2 .D USB FW:K2 "
[...]
I’ve stripped out a fair bit of the output, but as you can see we have some things to work with. The idVendor and idProduct will identify the device as a UPS, and the serial number will give a nice unique id for this UPS. Lets write a udev rule! I stuck it into the file /etc/udev/rules.d/53-nut-usbups-systemd.rules
ACTION!="add|change", GOTO="nut-usbups-systemd_rules_end"
SUBSYSTEM=="usb_device", GOTO="nut-usbups-systemd_rules_real"
SUBSYSTEM=="usb", GOTO="nut-usbups-systemd_rules_real"
SUBSYSTEM!="usb", GOTO="nut-usbups-systemd_rules_end"
LABEL="nut-usbups-systemd_rules_real"
ATTR{idVendor}=="051d", ATTR{idProduct}=="0002", TAG+="systemd", SYMLINK+="ups/$attr{serial}"
LABEL="nut-usbups-systemd_rules_end"
With this, my UPS is now available with device file /dev/ups/4B1111P37995
. And since I’ve tagged it with “systemd”, we will be able hook it up to our systemd unit. In order to keep things matching up, I defined the driver in my ups.conf as follows:
[apc]
driver = usbhid-ups
port = auto
serial = "4B1111P37995"
Ok, lets go and write the systemd unit file to start up the driver for my “apc” UPS device. I’m going to make use of another cool systemd feature here, template unit files and instances. Create a unit file named nut-driver@.service
, with the following content:
[Unit]
Description=Network UPS Tools - power device driver controller
[Service]
ExecStart=/usr/sbin/upsdrvctl start %i
ExecStop=/usr/sbin/upsdrvctl stop %i
Type=forking
So far, so good! Now here’s the clever bit. Right now, systemd is exposing a unit for the device, but it’s using the name sys-devices-pci0000:00-0000:00:1a.0-usb3-3\x2d1-3\x2d1.1.device
which is not only very ugly, but it’s also dependent on the usb path and port probe order. Lets create a unit with our new device name, in the file /etc/systemd/system/dev-ups-4B1111P37995.device
:
[Unit]
Description=APC Back-UPS ES 550_FW
You don’t actually need any more in the file; systemd will handle hooking it up to the appropriate real device automatically. But now we can hook up our unit file to be automatically started when the ups becomes available:
# mkdir /etc/systemd/system/dev-ups-4B1111P37995.device.wants
# ln -s ../nut-driver@.service /etc/systemd/system/dev-ups-4B1111P37995.device.wants/nut-driver@apc.service
Note that in the symlink that was created, I’ve added the string “apc” after the @ sign. When the unit is started, this will be substituted for the %i
in the nut-driver@ template unit file. This will cause the upsdrvctl command to look for the driver named “apc” in ups.conf, thus completing the chain and starting the the appropriate driver.
And now my UPS drivers reliably start, even when my computer can boot faster than USB can probe.
The unfortunate part is that a lot of this work is fairly system-specific. I’m not sure that it is all suitable for use in the upstream nut package, due to the customization required. I’d appreciate any comments or thoughts.
Leave a Reply