nut: fix no permissions to use USB UPS, and more

When a USB UPS is first configured, the permissions on the device under
`/dev/bus/usb` have not yet been set to allow the nut user access. This
resulted in errors such as:

Fri Feb 13 23:39:01 2026 daemon.debug upsd[3504]: [D1] mainloop: UPS
[eco550ups] is not currently connected, trying to reconnect
Fri Feb 13 23:39:01 2026 daemon.debug upsd[3504]: [D1] mainloop: UPS
[eco550ups] is still not connected (FD -1)
Fri Feb 13 23:39:03 2026 daemon.debug upsd[3504]: [D1] mainloop: UPS
[eco550ups] is not currently connected, trying to reconnect
Fri Feb 13 23:39:03 2026 daemon.debug upsd[3504]: [D1] mainloop: UPS
[eco550ups] is still not connected (FD -1)

or

Fri Feb 13 23:38:44 2026 daemon.err usbhid-ups[3083]: No matching HID
UPS found
Fri Feb 13 23:38:49 2026 daemon.warn procd: failed adding instance
cgroup for nut-server: No error information
Fri Feb 13 23:38:49 2026 daemon.err usbhid-ups[3115]: libusb1: Could not
open any HID devices: insufficient permissions on everything
Fri Feb 13 23:38:49 2026 daemon.err usbhid-ups[3115]: No matching HID
UPS found
Fri Feb 13 23:38:54 2026 daemon.warn procd: failed adding instance
cgroup for nut-server: No error information

and upsd would enter a procd crashloop.

We fix that by looking in `sysfs` (under `/sys/devices`) to find the
correct USB device and set its ownership and permissions to allow acces
to the user the driver is running under.

Copilot complained about a few things

* nut-server.init had potential word-splitting issues in various spots.
* it also had some commands missing an argument
* improved documentation was required to clarify a dependency
* an incorrect sed could mangle names as well as remove the intended
  name

Additionally, while fixing those issues the author noticed that the case
of multiple UPS devices with the same vendorid:productid were not
correctly handled. A check of the serial number, if provided, was added
along with a fallback to allowing NUT communications with all UPS
devices with a given vendorid:productid, if no serial number was given.

Improve efficiency and decrease McCabe complexity of
ensure_usb_ups_access, while also fixing Copilot complaints.

$@ in case is a problem, and we only handle the first parameter in any
event, so change $@ to "$1"

Copilot caught a missing 2>&1 and we silence some shellcheck
false positives

Signed-off-by: Daniel F. Dickinson <dfdpublic@wildtechgarden.ca>
This commit is contained in:
Daniel F. Dickinson
2026-02-14 22:23:12 -05:00
committed by Michael Heimpold
parent 9cfa3f657d
commit 072f32fee3
2 changed files with 86 additions and 14 deletions

View File

@@ -2,6 +2,7 @@
# shellcheck shell=ash # shellcheck shell=ash
# shellcheck disable=SC2034
START=82 START=82
STOP=28 STOP=28
USE_PROCD=1 USE_PROCD=1
@@ -151,6 +152,7 @@ nut_upsmon_add() {
} }
build_config() { build_config() {
# shellcheck disable=SC2174
mkdir -m 0750 -p "$(dirname "$UPSMON_C")" mkdir -m 0750 -p "$(dirname "$UPSMON_C")"
config_load nut_monitor config_load nut_monitor
@@ -174,6 +176,7 @@ interface_triggers() {
config_get triggerlist "upsmon" triggerlist config_get triggerlist "upsmon" triggerlist
# shellcheck disable=SC1091
. "${IPKG_INSTROOT}"/lib/functions/network.sh . "${IPKG_INSTROOT}"/lib/functions/network.sh
if [ -n "$triggerlist" ]; then if [ -n "$triggerlist" ]; then
@@ -221,7 +224,7 @@ pgrepkill() {
[ $# -eq 2 ] || return 1 [ $# -eq 2 ] || return 1
pids="$(pgrep "$1")" pids="$(pgrep "$1" 2>/dev/null)" || return 0
for pid in $pids; do for pid in $pids; do
kill -"$2" "$pid" kill -"$2" "$pid"
@@ -259,7 +262,7 @@ reload_service() {
else else
if procd_running nut-monitor upsmon; then if procd_running nut-monitor upsmon; then
if [ -s "$PIDFILE" ]; then if [ -s "$PIDFILE" ]; then
upsmon -c stop | logger -t nut-monitor upsmon -c stop 2>&1 | logger -t nut-monitor
else else
pgrepkill upsmon TERM >/dev/null 2>/dev/null pgrepkill upsmon TERM >/dev/null 2>/dev/null
fi fi
@@ -270,7 +273,7 @@ reload_service() {
stop_service() { stop_service() {
if [ -s "$PIDFILE" ]; then if [ -s "$PIDFILE" ]; then
upsmon -c stop | logger -t nut-monitor upsmon -c stop 2>&1 | logger -t nut-monitor
procd_kill nut-monitor 2>/dev/null | logger -t nut-monitor procd_kill nut-monitor 2>/dev/null | logger -t nut-monitor
else else
pgrepkill upsmon TERM >/dev/null 2>/dev/null pgrepkill upsmon TERM >/dev/null 2>/dev/null

View File

@@ -276,6 +276,67 @@ build_config() {
[ -n "$RUNAS" ] && chgrp "$(id -gn "$RUNAS")" "$UPS_C" [ -n "$RUNAS" ] && chgrp "$(id -gn "$RUNAS")" "$UPS_C"
} }
ensure_usb_ups_access() {
local ups="$1"
local vendorid
local productid
local runas=nut
runas="$RUNAS"
config_load nut_server
config_get vendorid "$ups" vendorid
config_get productid "$ups" productid
config_get serial "$ups" serial
[ -n "$vendorid" ] || return
[ -n "$productid" ] || return
local NL='
'
find /sys/devices -name idVendor -a -path '*usb*'| while IFS="$NL" read -r vendor_path; do
local usb_bus usb_dev device_path
# Filter by vendor ID first
if [ "$(cat "$vendor_path" 2>/dev/null)" != "$vendorid" ]; then
continue
fi
device_path="$(dirname "$vendor_path")"
# Then filter by product ID
if [ "$(cat "$device_path/idProduct" 2>/dev/null)" != "$productid" ]; then
continue
fi
# Next filter by serial, if provided
if [ -n "$serial" ] && [ "$serial" != "$(cat "$device_path"/serial)" ]; then
continue
fi
usb_bus="$(printf "%03d" "$(cat "$device_path"/busnum)")"
usb_dev="$(printf "%03d" "$(cat "$device_path"/devnum)")"
# usb_bus and usb_dev must each be at least 001
# a missing value will be present as 000 due to 'printf "%03d"'
local MISSING_USB_NUM="000"
if [ "$usb_bus" != "$MISSING_USB_NUM" ] && [ "$usb_dev" != "$MISSING_USB_NUM" ]; then
chmod 0660 /dev/bus/usb/"$usb_bus"/"$usb_dev"
chown "${runas:-root}":"$(id -gn "${runas:-root}")" /dev/bus/usb/"$usb_bus"/"$usb_dev"
fi
# Serial numbers are defined as unique, so do not loop further if serial
# was present and matched
if [ -n "$serial" ]; then
break
# If a serial number is not provided we need all vendor:product matches
# to have permissions for NUT as we do not know the matching method here
fi
done
}
# Must be called from start_service
start_ups_driver() { start_ups_driver() {
local ups="$1" local ups="$1"
local requested="$2" local requested="$2"
@@ -295,8 +356,10 @@ start_ups_driver() {
return 0 return 0
fi fi
# Depends on config_load from start_service
srv_statepath srv_statepath
srv_runas srv_runas
ensure_usb_ups_access "$ups"
config_get driver "$ups" driver "usbhid-ups" config_get driver "$ups" driver "usbhid-ups"
procd_open_instance "$ups" procd_open_instance "$ups"
@@ -373,7 +436,8 @@ start_service() {
[ "$should_start_srv" = "1" ] || return 0 [ "$should_start_srv" = "1" ] || return 0
case $@ in # We only start one service (upsd or one driver) from a given invocation
case "$1" in
"") "")
config_foreach start_ups_driver driver config_foreach start_ups_driver driver
start_server_instance upsd start_server_instance upsd
@@ -382,7 +446,7 @@ start_service() {
start_server_instance upsd start_server_instance upsd
;; ;;
*) *)
config_foreach start_ups_driver driver "$@" config_foreach start_ups_driver driver "$1"
;; ;;
esac esac
} }
@@ -423,7 +487,7 @@ signal_instance() {
elif pgrep "$process_name" >/dev/null 2>/dev/null; then elif pgrep "$process_name" >/dev/null 2>/dev/null; then
procd_send_signal nut-server "$instance_name" "$signal" 2>&1 | logger -t nut-server procd_send_signal nut-server "$instance_name" "$signal" 2>&1 | logger -t nut-server
fi fi
if [ -n "$secondary_command" ] && procd_running "$instance_name"; then if [ -n "$secondary_command" ] && procd_running nut-server "$instance_name"; then
$secondary_command 2>&1 | logger -t nut-server $secondary_command 2>&1 | logger -t nut-server
fi fi
} }
@@ -454,7 +518,7 @@ stop_ups_driver() {
if procd_running nut-server "$ups"; then if procd_running nut-server "$ups"; then
signal_instance "$ups" "$driver" "/lib/nut/'${driver}' -c exit -a '${ups}'" "TERM" "${STATEPATH}/${driver}-${ups}.pid" signal_instance "$ups" "$driver" "/lib/nut/'${driver}' -c exit -a '${ups}'" "TERM" "${STATEPATH}/${driver}-${ups}.pid"
if procd_running nut-server upsd >/dev/null 2>&1; then if procd_running nut-server upsd >/dev/null 2>&1; then
signal_instance upsd upsd "upsd -c stop" "TERM" "${STATEPATH}/upsd.pid" "procd_kill upsd" signal_instance upsd upsd "upsd -c stop" "TERM" "${STATEPATH}/upsd.pid" "procd_kill nut-server upsd"
fi fi
fi fi
} }
@@ -515,8 +579,8 @@ reload_service() {
config_foreach stop_ups_driver driver config_foreach stop_ups_driver driver
# Also stop any driver instances which are no longer configured # Also stop any driver instances which are no longer configured
for instance in $(list_running_instances "nut-server"|sed -e 's/upsd//'); do for instance in $(list_running_instances "nut-server"); do
if procd_running nut-server "$instance" >/dev/null 2>&1; then if [ "$instance" != "upsd" ] && procd_running nut-server "$instance" >/dev/null 2>&1; then
procd_kill nut-server "$instance" 2>&1 | logger -t nut-server procd_kill nut-server "$instance" 2>&1 | logger -t nut-server
fi fi
done done
@@ -541,7 +605,10 @@ reload_service() {
# Stop any driver instances which are no longer configured # Stop any driver instances which are no longer configured
# We can only reliably do this for instances managed by procd # We can only reliably do this for instances managed by procd
for instance in $(list_running_instances "nut-server"|sed -e 's/upsd//'); do for instance in $(list_running_instances "nut-server"); do
if [ "$instance" = "upsd" ]; then
continue
fi
unset driver unset driver
config_get driver "$instance" driver config_get driver "$instance" driver
if [ -z "$driver" ] && procd_running nut-server "$instance" >/dev/null 2>&1; then if [ -z "$driver" ] && procd_running nut-server "$instance" >/dev/null 2>&1; then
@@ -558,7 +625,8 @@ stop_service() {
config_load nut_server config_load nut_server
srv_statepath srv_statepath
case $@ in # We only handle the first parameter passed
case "$1" in
"") "")
# If nut-server was started but has no instances (even upsd) # If nut-server was started but has no instances (even upsd)
if server_active; then if server_active; then
@@ -570,8 +638,8 @@ stop_service() {
config_foreach stop_ups_driver driver config_foreach stop_ups_driver driver
# Also stop any driver instances which are no longer configured # Also stop any driver instances which are no longer configured
for instance in $(list_running_instances "nut-server"|sed -e 's/upsd//'); do for instance in $(list_running_instances "nut-server"); do
if procd_running nut-server "$instance" >/dev/null 2>&1; then if [ "$instance" != "upsd" ] && procd_running nut-server "$instance" >/dev/null 2>&1; then
procd_kill nut-server "$instance" 2>&1 | logger -t nut-server procd_kill nut-server "$instance" 2>&1 | logger -t nut-server
fi fi
done done
@@ -592,7 +660,8 @@ stop_service() {
fi fi
;; ;;
*) *)
config_foreach stop_ups_driver driver "$@" # We only handle the first parameter, so do not pass in all parameters
config_foreach stop_ups_driver driver "$1"
;; ;;
esac esac
} }