From ea49955b8d0ddd371bd3ca53010f95f9cc1c14b4 Mon Sep 17 00:00:00 2001 From: "Daniel F. Dickinson" Date: Sat, 9 May 2026 04:49:52 -0400 Subject: [PATCH 1/7] nut: bump to 2.8.5 Bump version to latest stable release. Adjust configure and drivers as needed. Signed-off-by: Daniel F. Dickinson --- net/nut/Makefile | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/net/nut/Makefile b/net/nut/Makefile index fa5acf83bf928..3946ad370ee3c 100644 --- a/net/nut/Makefile +++ b/net/nut/Makefile @@ -8,12 +8,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=nut -PKG_VERSION:=2.8.4 -PKG_RELEASE:=3 +PKG_VERSION:=2.8.5 +PKG_RELEASE:=1 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz -PKG_SOURCE_URL:=https://networkupstools.org/source/2.8/ -PKG_HASH:=a2fe55bc2d90b4a848d6ff8bac361e6d1c97f899a545219cad707d17a27ff127 +PKG_SOURCE_URL:=https://www.networkupstools.org/source/2.8/ +PKG_HASH:=18bf32e59eb764b13da3c4fa70384926d7fa584cb31d2fe7f137a570633eeec1 PKG_LICENSE:=GPL-2.0-or-later GPL-3.0-or-later GPL-1.0-or-later Artistic-1.0-Perl PKG_LICENSE_FILES:=LICENSE-GPL2 LICENSE-GPL3 COPYING PKG_FIXUP:=autoreconf @@ -406,7 +406,7 @@ SERIAL_DRIVERLIST = al175 bcmxcp belkin belkinunv bestfcom \ gamatronic genericups isbmex liebert liebert-esp2 liebert-gxe masterguard metasys \ mge-utalk microdowell microsol-apc mge-shut nutdrv_hashx oneac optiups powercom powervar_cx_ser rhino \ safenet nutdrv_siemens-sitop solis tripplite tripplitesu upscode2 victronups powerpanel \ - blazer_ser ivtscd apcsmart apcsmart-old riello_ser sms_ser bicker_ser ve-direct \ + blazer_ser ivtscd apcsmart apcsmart-old riello_ser sms_ser bicker_ser ve-direct meanwell_ntu \ nutdrv_qx SERIAL_DRIVERLIST += nhs_ser SNMP_DRIVERLIST = snmp-ups @@ -527,8 +527,10 @@ $(eval $(call DriverDescription,serial,bicker_ser,\ Driver for Bicker DC UPS via serial port connections)) $(eval $(call DriverDescription,serial,ve-direct,\ Driver for Victron UPS unit running on VE.Direct serial protocol)) +$(eval $(call DriverDescription,serial,meanwell_ntu,\ + Driver for Mean Well NTU series equipment with serial port)) $(eval $(call DriverDescription,serial,nhs_ser,\ - Driver for NHS Nobreaks, senoidal line, with serial port)) + Driver for NHS Nobreaks - senoidal line - with serial port)) $(eval $(call DriverDescription,snmp,snmp-ups,\ Multi-MIB Driver for SNMP UPS equipment)) $(eval $(call DriverDescription,usb,usbhid-ups,\ @@ -556,7 +558,8 @@ CONFIGURE_VARS += \ ac_cv_path_AR=$(TARGET_AR) CONFIGURE_ARGS += \ - --sysconfdir=/etc/nut \ + --sysconfdir=/etc \ + --with-confdir-suffix=/nut \ --datadir=/usr/share/nut \ --with-dev \ --$(if $(CONFIG_NUT_DRIVER_USB),with,without)-usb \ From 2ec1ddf050fcabeb1133917f844f90df726c3c3b Mon Sep 17 00:00:00 2001 From: "Daniel F. Dickinson" Date: Sat, 9 May 2026 22:33:24 -0400 Subject: [PATCH 2/7] nut: move drivers to libexec They are executables not libraries, so move the UPS drivers to /usr/libexec/nut. Signed-off-by: Daniel F. Dickinson --- net/nut/Makefile | 8 ++++---- net/nut/files/nut-server.init | 6 +++--- net/nut/files/nutshutdown | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/net/nut/Makefile b/net/nut/Makefile index 3946ad370ee3c..58643b458bd7e 100644 --- a/net/nut/Makefile +++ b/net/nut/Makefile @@ -383,9 +383,9 @@ define DriverPackage endef define Package/nut-driver-$(2)/install - $(INSTALL_DIR) $$(1)/lib/nut - $(CP) $$(PKG_INSTALL_DIR)/lib/nut/$(2) $$(1)/lib/nut/ - $(if $(filter $(2),clone),$(CP) $$(PKG_INSTALL_DIR)/lib/nut/$(2)-outlet $$(1)/lib/nut/) + $(INSTALL_DIR) $$(1)/usr/libexec/nut + $(CP) $$(PKG_INSTALL_DIR)/usr/libexec/nut/$(2) $$(1)/usr/libexec/nut/ + $(if $(filter $(2),clone),$(CP) $$(PKG_INSTALL_DIR)/usr/libexec/nut/$(2)-outlet $$(1)/usr/libexec/nut/) endef endef define DriverDescription @@ -582,7 +582,7 @@ CONFIGURE_ARGS += \ --without-nut_monitor \ --with-statepath=/var/run/nut \ --with-pidpath=/var/run \ - --with-drvpath=/lib/nut \ + --with-drvpath=/usr/libexec/nut \ --with-user=nut \ --with-group=nut \ $(if $(CONFIG_PACKAGE_nut-web-cgi),--with-gd-includes="`pkg-config --cflags gdlib`") \ diff --git a/net/nut/files/nut-server.init b/net/nut/files/nut-server.init index be1cd45e20a5c..e62a3c364f02b 100755 --- a/net/nut/files/nut-server.init +++ b/net/nut/files/nut-server.init @@ -368,7 +368,7 @@ start_ups_driver() { procd_set_param stdout 0 # Subset of stderr procd_set_param env NUT_QUIET_INIT_UPSNOTIFY=true procd_set_param env NUT_STATEPATH="${STATEPATH}" - procd_set_param command /lib/nut/"${driver}" -FF -a "$ups" ${RUNAS:+-u "$RUNAS"} + procd_set_param command /usr/libexec/nut/"${driver}" -FF -a "$ups" ${RUNAS:+-u "$RUNAS"} procd_close_instance haveupscfg=1 } @@ -516,7 +516,7 @@ stop_ups_driver() { config_get driver "$ups" driver "usbhid-ups" 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" "/usr/libexec/nut/'${driver}' -c exit -a '${ups}'" "TERM" "${STATEPATH}/${driver}-${ups}.pid" if procd_running nut-server upsd >/dev/null 2>&1; then signal_instance upsd upsd "upsd -c stop" "TERM" "${STATEPATH}/upsd.pid" "procd_kill nut-server upsd" fi @@ -546,7 +546,7 @@ reload_ups_driver() { # Try to reload, otherwise exit politely, then stop and restart procd instance if procd_running nut-server "$ups"; then - signal_instance "$ups" "$driver" "/lib/nut/'${driver}' -c reload-or-exit -a '${ups}'" HUP "${STATEPATH}/${driver}-${ups}.pid" + signal_instance "$ups" "$driver" "/usr/libexec/nut/'${driver}' -c reload-or-exit -a '${ups}'" HUP "${STATEPATH}/${driver}-${ups}.pid" fi /etc/init.d/nut-server start "$ups" 2>&1 | logger -t nut-server } diff --git a/net/nut/files/nutshutdown b/net/nut/files/nutshutdown index 7270f983f1088..f8115270465f9 100755 --- a/net/nut/files/nutshutdown +++ b/net/nut/files/nutshutdown @@ -14,7 +14,7 @@ shutdown_instance() { # Only FSD if killpower was indicated if [ -f /var/run/killpower ]; then - /lib/nut/"${driver}" -a "$cfg" -k + /usr/libexec/nut/"${driver}" -a "$cfg" -k fi } From 66a1b7108c9adc3b3d641d3008d5190f6b4525eb Mon Sep 17 00:00:00 2001 From: "Daniel F. Dickinson" Date: Sat, 9 May 2026 11:27:18 -0400 Subject: [PATCH 3/7] nut: bump OpenWrt copyright for files with it For files with an existing OpenWrt copyright notation, update to include 2026 for scripts which have been updated this year. Per https://github.com/openwrt/packages/pull/29390#discussion_r3213318908 Signed-off-by: Daniel F. Dickinson --- net/nut/Makefile | 2 +- net/nut/files/nut-cgi.init | 2 +- net/nut/files/nut-server.init | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/net/nut/Makefile b/net/nut/Makefile index 58643b458bd7e..2a8f2d7458845 100644 --- a/net/nut/Makefile +++ b/net/nut/Makefile @@ -1,5 +1,5 @@ -# Copyright (C) 2006-2016 OpenWrt.org +# Copyright (C) 2006-2026 OpenWrt.org # # This is free software, licensed under the GNU General Public License v2. # See /LICENSE for more information. diff --git a/net/nut/files/nut-cgi.init b/net/nut/files/nut-cgi.init index 68d39f668b85b..e5c3cf193dd87 100755 --- a/net/nut/files/nut-cgi.init +++ b/net/nut/files/nut-cgi.init @@ -1,5 +1,5 @@ #!/bin/sh /etc/rc.common -# Copyright © 2012 OpenWrt.org +# Copyright © 2012-2026 OpenWrt.org # # This is free software, licensed under the GNU General Public License v2. # See /LICENSE for more information. diff --git a/net/nut/files/nut-server.init b/net/nut/files/nut-server.init index e62a3c364f02b..dcdd08cff0734 100755 --- a/net/nut/files/nut-server.init +++ b/net/nut/files/nut-server.init @@ -1,5 +1,5 @@ #!/bin/sh /etc/rc.common -# Copyright © 2012 OpenWrt.org +# Copyright © 2012-2026 OpenWrt.org # # This is free software, licensed under the GNU General Public License v2. # See /LICENSE for more information. From 331168a6a327c30f18b393a125b28ad8a57220b4 Mon Sep 17 00:00:00 2001 From: "Daniel F. Dickinson" Date: Sat, 9 May 2026 22:35:51 -0400 Subject: [PATCH 4/7] nut: add version test overrides Allow CI to pass by skipping the generic version check where it not appropriate. Signed-off-by: Daniel F. Dickinson --- net/nut/test-version.sh | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 net/nut/test-version.sh diff --git a/net/nut/test-version.sh b/net/nut/test-version.sh new file mode 100644 index 0000000000000..2b07817d0a98d --- /dev/null +++ b/net/nut/test-version.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +if [ "$PKG_NAME" = "nut" ]; then + exit 0 +fi + +EXEC="${PKG_NAME#nut-}" + +case "$EXEC" in +common | upsmon-sendmail-notify | avahi-service) + exit 0 + ;; +driver-*) + DRIVER="${EXEC#driver-}" + /usr/libexec/nut/"$DRIVER" -V 2>&1 | grep -qF "${PKG_VERSION}" + ;; +server) + "upsd" -V 2>&1 | grep -qF "${PKG_VERSION}" && "upsdrvctl" -V 2>&1 | grep -qF "${PKG_VERSION}" + ;; +upssched) + # Only intended to be run from upsmon + exit 0 + ;; +web-cgi) + # Only runs as CGI scripts + exit 0 + ;; +*) + "$EXEC" -V 2>&1 | grep -qF "${PKG_VERSION}" + ;; +esac From cb7a26a4e7a071e132c30883c0e8abc5d9d504a5 Mon Sep 17 00:00:00 2001 From: "Daniel F. Dickinson" Date: Sat, 30 May 2026 18:23:15 -0400 Subject: [PATCH 5/7] nut: belatedly add migration for nut-monitor changes Add previously missed migration script (uci-defaults) for changes to nut-monitor initscript. Created with the help of iterative code reviews by Qwen3.6-27B (LLM model) running on llama.cpp (local LLM server), controlled by LATE. Signed-off-by: Daniel F. Dickinson --- net/nut/Makefile | 2 + net/nut/files/nut-monitor-migrate.default | 243 ++++++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 net/nut/files/nut-monitor-migrate.default diff --git a/net/nut/Makefile b/net/nut/Makefile index 2a8f2d7458845..04d12d5364db8 100644 --- a/net/nut/Makefile +++ b/net/nut/Makefile @@ -170,9 +170,11 @@ define Package/nut-upsmon/install $(INSTALL_DIR) $(1)/etc/nut $(INSTALL_DIR) $(1)/usr/sbin $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_DIR) $(1)/etc/uci-defaults $(INSTALL_BIN) ./files/nut-monitor.init $(1)/etc/init.d/nut-monitor $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/sbin/upsmon $(1)/usr/sbin/ $(INSTALL_BIN) ./files/nutshutdown $(1)/usr/sbin/nutshutdown + $(INSTALL_DATA) ./files/nut-monitor-migrate.default $(1)/etc/uci-defaults/80-nut-monitor-migrate $(INSTALL_DIR) $(1)/etc/config $(INSTALL_CONF) ./files/nut_monitor $(1)/etc/config/nut_monitor ln -sf /var/etc/nut/upsmon.conf $(1)/etc/nut/upsmon.conf diff --git a/net/nut/files/nut-monitor-migrate.default b/net/nut/files/nut-monitor-migrate.default new file mode 100644 index 0000000000000..2d928127c6290 --- /dev/null +++ b/net/nut/files/nut-monitor-migrate.default @@ -0,0 +1,243 @@ +#!/bin/sh +# Script is sourced not executed, but shebang helps tools recognize this as +# a shell script. + +# In recent (relevant) versions of shellcheck busybox is a valid shell type +# shellcheck shell=busybox + +# uci-defaults script to migrate old uci config for notifications and monitor sections, +# to the new. Applied during install or on first boot after install, if setting on install +# fails + +# IPKG_INSTROOT is intentionally only set when building an image and +# is intentionally empty on a live OpenWrt device + +# Only run this uci-defaults script on a live OpenWrt device +[ -z "${IPKG_INSTROOT}" ] || exit 0 + +# Loads needed OpenWrt functions +# shellcheck source=net/nut/files/functions.sh.functions +. /lib/functions.sh || { + logger -s -t nut-monitor-migrate "Unable to source 'functions.sh' in 'nut-monitor-migrate' uci-default script. Bailing" + exit 1 +} + +# Adds notification flag (value) to 'notify_flags' from caller +# shellcheck disable=SC2329,SC2317 +get_notify_flags() { + local value="$1" + + append notify_flags "$value" "+" +} + +# shellcheck disable=SC2329,SC2317 +convert_flags() { + local notify_flags="$1" + local working_flags="" + local flag_val + + # In input flags can be in any order. If any of the input flags are IGNORE, + # the output should only contain IGNORE. If the input contains only SYSLOG + # or only EXEC, use as is. If the input contains both SYSLOG and EXEC (in + # any order) the output must be SYSLOG+EXEC + for flag_val in IGNORE SYSLOG EXEC; do + # If the notify_flags contains $flag_val + case "$notify_flags" in + "$flag_val"+* | *+"$flag_val" | "$flag_val") + case "$flag_val" in + IGNORE) + working_flags="IGNORE" + printf "%s" "$working_flags" + return + ;; + SYSLOG) + # If working flags exists, it already has a non SYSLOG value + # SYSLOG must come first in the final flags, so append + # old working_flags to SYSLOG with delimiter '+', and make + # that the new working_flags + if [ "$working_flags" = "EXEC" ]; then + working_flags="${flag_val}+${working_flags}" + else + working_flags="$flag_val" + fi + ;; + EXEC) + # append to an empty variable just returns the value to be + # appended without a delimiter, so we do not need to check + # for already having a value in 'working_flags' + append working_flags "$flag_val" "+" + ;; + esac + ;; + esac + done + printf "%s" "$working_flags" +} + +# As the uci-defaults environment in which this runs does not logging available, +# nor is stderr captured or displayed on the console, these messages exist only +# to assist when debugging manual runs of the script. +# shellcheck disable=SC2329,SC2317 +log_migration_error() { + local reason="$1" + + printf "'%s': ERROR: '%s'\n" "nut-monitor-migrate" "$reason" >&2 +} + +# shellcheck disable=SC2329,SC2317 +get_notification_flags() { + local uci_option="$1" + local cfg="$2" + local notify_flags="" + # accumulates flags in notify_flags via get_notify_flags + config_list_foreach "$cfg" "$uci_option" get_notify_flags + convert_flags "$notify_flags" +} + +# shellcheck disable=SC2329,SC2317 +process_upsmon_section() { + local cfg="$1" + local notify_type flag_name notify_msg notify_section + local notification_name notify_flags_as_var upsmon_final_flags + # List of notification messages options in existing (to be converted) + # UCI config for nut_monitor + local nut_notify_message_types="onlinemsg onbattmsg lowbattmsg fsdmsg commokmsg" + append nut_notify_message_types "commbadmsg shutdownmsg replbattmsg" + append nut_notify_message_types "nocommmsmsg nocommmsg noparentmsg" + + # We need word-splitting to iterate over nut_notify_message_types + # This is a local variable defined by us, so it is safe to do this. + # We still disable globbing of the word-split + set -f + for notify_type in $nut_notify_message_types; do + # Transform msg into notify (flag_name) and (notification_name) + flag_name="${notify_type/msg/notify}" + notification_name="${notify_type/msg/}" + # busybox ash does not support case transformation via variable parameter substitution, + # so we use tr. + notification_name="$(printf '%s' "$notification_name" | tr '[:lower:]' '[:upper:]')" + # Transform wrong NOCOMMS to NOCOMM (typo in nocommsmsg which should + # have been nocommmsg in previous script) + if [ "$notification_name" = "NOCOMMMS" ]; then + notification_name="NOCOMM" + fi + config_get notify_msg "$cfg" "$notify_type" + config_get notify_flags_as_var "$cfg" "$flag_name" + if [ -n "$notify_msg" ] || [ -n "$notify_flags_as_var" ]; then + notify_section="$(uci -q add nut_monitor notifications)" + if [ -z "$notify_section" ]; then + log_migration_error "Failed to add notifications section" + # We don't want to prevent other sections from being processed + # if this one fails, so do not error exit, but keep going. + continue + fi + if ! uci -q rename nut_monitor."$notify_section"="$notification_name"; then + log_migration_error "Failed to rename '$notify_section' to '$notification_name'" + # We don't want to prevent other sections from being processed + # if this one fails, so do not error exit, but keep going. + continue + fi + fi + if [ -n "$notify_msg" ]; then + if ! uci -q set nut_monitor."$notification_name".message="$notify_msg"; then + log_migration_error "Failed to add message for '$notification_name'" + continue + fi + fi + upsmon_final_flags="$(get_notification_flags "$flag_name" "$cfg")" + if [ -n "$upsmon_final_flags" ]; then + if ! uci -q set nut_monitor."$notification_name".flag="$upsmon_final_flags"; then + log_migration_error "Failed to add flag(s) to '$notification_name'" + continue + fi + fi + + # Keep going even if deleting old flag fails. It won't be used + # anyway, and partial success is better than none. + if [ -n "$cfg" ] && [ -n "$notify_flags_as_var" ] && ! uci -q delete nut_monitor."$cfg"."$flag_name"; then + log_migration_error "Failed to delete old flag '$flag_name' from '$cfg'" + fi + # Keep going even if deleting old notification message fails. It + # won't be used anyway, and partial success is better than none. + if [ -n "$cfg" ] && [ -n "$notify_msg" ] && ! uci -q delete nut_monitor."$cfg"."$notify_type"; then + log_migration_error "Failed to delete old notification type '$notify_type' from '$cfg'" + fi + done + set +f + upsmon_final_flags="$(get_notification_flags "defaultnotify" "$cfg")" + if [ -n "$upsmon_final_flags" ]; then + if ! uci -q set nut_monitor."$cfg".defaultnotify="$upsmon_final_flags"; then + log_migration_error "Failed to update defaultnotify for '$cfg'" + fi + fi +} + +# shellcheck disable=SC2329,SC2317 +process_primary_secondary() { + local cfg="$1" + local monitor_type="$2" + local upsname hostname port powervalue username password section + + section="$(uci -q add nut_monitor monitor)" || { + log_migration_error "Failed to add monitor section to replace '$cfg'" + # Do not error exit as we do not want to stop other sections from being + # processed + return 0 + } + + if [ -n "$section" ]; then + { + uci -q set nut_monitor."$section".type="$monitor_type" || return 1 + + # Empty values are allowed and should remain as such in the new + # section. + config_get upsname "$cfg" upsname + config_get hostname "$cfg" hostname + config_get port "$cfg" port + config_get powervalue "$cfg" powervalue + config_get username "$cfg" username + config_get password "$cfg" password + + uci -q set nut_monitor."$section".upsname="$upsname" || return 1 + uci -q set nut_monitor."$section".hostname="$hostname" || return 1 + uci -q set nut_monitor."$section".port="$port" || return 1 + uci -q set nut_monitor."$section".powervalue="$powervalue" || return 1 + uci -q set nut_monitor."$section".username="$username" || return 1 + uci -q set nut_monitor."$section".password="$password" || return 1 + + # Delete the old section with name config (of type primary + # or secondary) + uci -q delete nut_monitor."$cfg" || return 1 + # Rename the nut_monitor section '$section' we created, above, with + # the name of the old section. This requires that the old section + # with the same name has already been removed. + # In the event of delete failure, the new section is present, but + # with the name assigned when the new section was created. + uci -q rename nut_monitor."$section"="$cfg" || return 1 + return 0 + } || { + log_migration_error "Error converting primary/secondary section '$cfg'" + # Do not error exit as we do not want to stop other sections from + # being processed + return 0 + } + else + log_migration_error "Name of monitor section to replace '$cfg' was empty" + # Do not error exit as we do not want to stop other sections from being + # processed + return 0 + + fi +} + +config_load nut_monitor || { + echo "nut-monitor-migrate: FATAL: Failed to load nut_monitor" + exit 1 +} +config_foreach process_upsmon_section upsmon +config_foreach process_primary_secondary master primary +config_foreach process_primary_secondary slave secondary + +uci commit nut_monitor + +exit 0 From f59aa50c87931543e4cf40c1b8a885280eab03bf Mon Sep 17 00:00:00 2001 From: "Daniel F. Dickinson" Date: Tue, 2 Jun 2026 16:47:28 -0400 Subject: [PATCH 6/7] nut: fix quoting bug for ups stop and reload Extra quotes were being interpreted literally, preventing proper ups driver stop and/or reload. Signed-off-by: Daniel F. Dickinson --- net/nut/files/nut-server.init | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/net/nut/files/nut-server.init b/net/nut/files/nut-server.init index dcdd08cff0734..6365f60eb6b72 100755 --- a/net/nut/files/nut-server.init +++ b/net/nut/files/nut-server.init @@ -516,7 +516,7 @@ stop_ups_driver() { config_get driver "$ups" driver "usbhid-ups" if procd_running nut-server "$ups"; then - signal_instance "$ups" "$driver" "/usr/libexec/nut/'${driver}' -c exit -a '${ups}'" "TERM" "${STATEPATH}/${driver}-${ups}.pid" + signal_instance "$ups" "$driver" "/usr/libexec/nut/${driver} -c exit -a ${ups}" "TERM" "${STATEPATH}/${driver}-${ups}.pid" if procd_running nut-server upsd >/dev/null 2>&1; then signal_instance upsd upsd "upsd -c stop" "TERM" "${STATEPATH}/upsd.pid" "procd_kill nut-server upsd" fi @@ -546,7 +546,7 @@ reload_ups_driver() { # Try to reload, otherwise exit politely, then stop and restart procd instance if procd_running nut-server "$ups"; then - signal_instance "$ups" "$driver" "/usr/libexec/nut/'${driver}' -c reload-or-exit -a '${ups}'" HUP "${STATEPATH}/${driver}-${ups}.pid" + signal_instance "$ups" "$driver" "/usr/libexec/nut/${driver} -c reload-or-exit -a {ups}" HUP "${STATEPATH}/${driver}-${ups}.pid" fi /etc/init.d/nut-server start "$ups" 2>&1 | logger -t nut-server } From e761068d7530448d45818a08d300f95b2b881c32 Mon Sep 17 00:00:00 2001 From: "Daniel F. Dickinson" Date: Thu, 7 May 2026 21:32:59 -0400 Subject: [PATCH 7/7] nut: rewrite scripts The scripts were a mess. Attempting even a simple update caused many Copilot complaints. So we rewrite the scripts to be cleaner and resolve the issues found by automated code review (such as Copilot). Made extensive use of Qwen3.6-27B, LATE, and llama.ccp for local AI code reviews during development. In the process we deduplicate the nut-server and nut-monitor initscripts and split them into several files, for easier automatic and human review. Incorporates and supersedes: #28308 Should supersede #21014 Closes: #28298 Signed-off-by: Daniel F. Dickinson --- net/nut/Makefile | 39 +- net/nut/files/30-libhid-ups.head | 49 - net/nut/files/30-libhid-ups.tail | 5 - net/nut/files/add_nut_httpd_conf | 6 - net/nut/files/add_nut_httpd_conf.default | 27 + net/nut/files/libhid-ups.hotplug | 347 +++++++ net/nut/files/nut-cgi.init | 337 ++++++- net/nut/files/nut-common.sh.functions | 193 ++++ net/nut/files/nut-monitor-config.sh.functions | 339 +++++++ net/nut/files/nut-monitor.init | 394 +++----- net/nut/files/nut-notify-exec.default | 78 ++ net/nut/files/nut-sched.default | 98 +- net/nut/files/nut-sendmail-notify | 129 ++- net/nut/files/nut-sendmail-notify.default | 101 +- net/nut/files/nut-serial.hotplug | 191 ++++ net/nut/files/nut-server-config.sh.functions | 386 ++++++++ net/nut/files/nut-server-service.sh.functions | 202 ++++ net/nut/files/nut-server.init | 925 +++++++----------- net/nut/files/nut-service.sh.functions | 333 +++++++ net/nut/files/nut_cgi | 2 +- net/nut/files/nut_monitor | 27 +- net/nut/files/nut_serial.hotplug | 34 - net/nut/files/nut_server | 14 +- net/nut/files/nutshutdown | 225 ++++- 24 files changed, 3407 insertions(+), 1074 deletions(-) delete mode 100755 net/nut/files/30-libhid-ups.head delete mode 100644 net/nut/files/30-libhid-ups.tail delete mode 100644 net/nut/files/add_nut_httpd_conf create mode 100644 net/nut/files/add_nut_httpd_conf.default create mode 100644 net/nut/files/libhid-ups.hotplug mode change 100755 => 100644 net/nut/files/nut-cgi.init create mode 100644 net/nut/files/nut-common.sh.functions create mode 100644 net/nut/files/nut-monitor-config.sh.functions create mode 100644 net/nut/files/nut-notify-exec.default mode change 100755 => 100644 net/nut/files/nut-sendmail-notify create mode 100644 net/nut/files/nut-serial.hotplug create mode 100644 net/nut/files/nut-server-config.sh.functions create mode 100644 net/nut/files/nut-server-service.sh.functions mode change 100755 => 100644 net/nut/files/nut-server.init create mode 100644 net/nut/files/nut-service.sh.functions delete mode 100644 net/nut/files/nut_serial.hotplug mode change 100755 => 100644 net/nut/files/nutshutdown diff --git a/net/nut/Makefile b/net/nut/Makefile index 04d12d5364db8..454779f15d22d 100644 --- a/net/nut/Makefile +++ b/net/nut/Makefile @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=nut PKG_VERSION:=2.8.5 -PKG_RELEASE:=1 +PKG_RELEASE:=2 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://www.networkupstools.org/source/2.8/ @@ -69,9 +69,12 @@ define Package/nut-server/install $(INSTALL_DIR) $(1)/etc/nut $(INSTALL_DIR) $(1)/usr/sbin $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_DIR) $(1)/lib/functions/nut $(INSTALL_DIR) $(1)/usr/share/nut $(INSTALL_BIN) ./files/nut-server.init $(1)/etc/init.d/nut-server - $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/sbin/upsd $(1)/usr/sbin + $(INSTALL_DATA) ./files/nut-server-config.sh.functions $(1)/lib/functions/nut/nut-server-config.sh + $(INSTALL_DATA) ./files/nut-server-service.sh.functions $(1)/lib/functions/nut/nut-server-service.sh + $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/sbin/upsd $(1)/usr/sbin/ $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/share/nut/cmdvartab $(1)/usr/share/nut/ $(INSTALL_DIR) $(1)/etc/config $(INSTALL_CONF) ./files/nut_server $(1)/etc/config/nut_server @@ -84,15 +87,15 @@ define Package/nut-server/install # Mangle libhid.usermap into a format (hotplug shell script) useful for OpenWrt $(INSTALL_DIR) $(1)/etc/hotplug.d/usb $(INSTALL_DIR) $(1)/etc/hotplug.d/tty - $(INSTALL_BIN) ./files/nut_serial.hotplug $(1)/etc/hotplug.d/tty/40-nut_serial - $(INSTALL_BIN) ./files/30-libhid-ups.head $(1)/etc/hotplug.d/usb/30-libhid-ups - $(CP) $(PKG_INSTALL_DIR)/etc/hotplug/usb/libhid.usermap $(PKG_BUILD_DIR)/30-libhid-ups.middle + $(INSTALL_BIN) ./files/nut-serial.hotplug $(1)/etc/hotplug.d/tty/40-nut-serial + $(CP) ./files/libhid-ups.hotplug $(PKG_BUILD_DIR)/30-libhid-ups + $(CP) $(PKG_INSTALL_DIR)/etc/hotplug/usb/libhid.usermap $(PKG_BUILD_DIR)/libhid-ups.parsed-usermap $(SED) '/^$$$$/d' \ -e '/^#/d' \ -E -e 's:^[^ ][^ ]* *0x0003 *0x0{0,3}([^ ][^ ]*) *0x0{0,3}*([^ ][^ ]*).*:\1/\2/* | \\:' \ - $(PKG_BUILD_DIR)/30-libhid-ups.middle - tail -n+2 $(PKG_BUILD_DIR)/30-libhid-ups.middle >>$(1)/etc/hotplug.d/usb/30-libhid-ups - cat ./files/30-libhid-ups.tail >>$(1)/etc/hotplug.d/usb/30-libhid-ups + $(PKG_BUILD_DIR)/libhid-ups.parsed-usermap + $(SED) 's^### insert libhid-ups.parsed-usermap content here ###^\ncat "$(PKG_BUILD_DIR)/libhid-ups.parsed-usermap"^e' $(PKG_BUILD_DIR)/30-libhid-ups + $(INSTALL_BIN) $(PKG_BUILD_DIR)/30-libhid-ups $(1)/etc/hotplug.d/usb/30-libhid-ups endef define Package/nut-common @@ -117,6 +120,9 @@ endef define Package/nut-common/install $(INSTALL_DIR) $(1)/etc/nut $(INSTALL_DIR) $(1)/usr/lib + $(INSTALL_DIR) $(1)/lib/functions/nut + $(INSTALL_DATA) ./files/nut-common.sh.functions $(1)/lib/functions/nut/nut-common.sh + $(INSTALL_DATA) ./files/nut-service.sh.functions $(1)/lib/functions/nut/nut-service.sh $(CP) $(PKG_INSTALL_DIR)/usr/lib/libupsclient.so* $(1)/usr/lib/ ln -sf /var/etc/nut/nut.conf $(1)/etc/nut/nut.conf endef @@ -170,8 +176,10 @@ define Package/nut-upsmon/install $(INSTALL_DIR) $(1)/etc/nut $(INSTALL_DIR) $(1)/usr/sbin $(INSTALL_DIR) $(1)/etc/init.d - $(INSTALL_DIR) $(1)/etc/uci-defaults + $(INSTALL_DIR) $(1)/lib/functions/nut + $(INSTALL_DIR) $(1)/usr/bin $(1)/etc/uci-defaults $(INSTALL_BIN) ./files/nut-monitor.init $(1)/etc/init.d/nut-monitor + $(INSTALL_DATA) ./files/nut-monitor-config.sh.functions $(1)/lib/functions/nut/nut-monitor-config.sh $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/sbin/upsmon $(1)/usr/sbin/ $(INSTALL_BIN) ./files/nutshutdown $(1)/usr/sbin/nutshutdown $(INSTALL_DATA) ./files/nut-monitor-migrate.default $(1)/etc/uci-defaults/80-nut-monitor-migrate @@ -195,7 +203,8 @@ endef define Package/nut-upsmon-sendmail-notify/install $(INSTALL_DIR) $(1)/usr/bin $(1)/etc/uci-defaults $(INSTALL_BIN) ./files/nut-sendmail-notify $(1)/usr/bin/ - $(INSTALL_DATA) ./files/nut-sendmail-notify.default $(1)/etc/uci-defaults/nut-sendmail-notify + $(INSTALL_DATA) ./files/nut-sendmail-notify.default $(1)/etc/uci-defaults/81-nut-sendmail-notify + $(INSTALL_DATA) ./files/nut-notify-exec.default $(1)/etc/uci-defaults/71-nut-notify-exec endef define Package/nut-upsc @@ -301,7 +310,8 @@ define Package/nut-upssched/install $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/upssched-cmd $(1)/usr/bin/ $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/sbin/upssched $(1)/usr/sbin/ $(INSTALL_CONF) $(PKG_INSTALL_DIR)/etc/nut/upssched.conf.sample $(1)/etc/nut/upssched.conf - $(INSTALL_DATA) ./files/nut-sched.default $(1)/etc/uci-defaults/nut-upssched + $(INSTALL_DATA) ./files/nut-sched.default $(1)/etc/uci-defaults/80-nut-upssched + $(INSTALL_DATA) ./files/nut-notify-exec.default $(1)/etc/uci-defaults/70-nut-notify-exec endef define Package/nut-web-cgi @@ -331,12 +341,9 @@ define Package/nut-web-cgi/install $(CP) $(PKG_INSTALL_DIR)/usr/html/* $(1)/www/nut/ $(INSTALL_DIR) $(1)/etc/uci-defaults $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/cgi-bin/* $(1)/www/cgi-bin/nut - $(INSTALL_CONF) ./files/add_nut_httpd_conf $(1)/etc/uci-defaults/add_nut_httpd_conf + $(INSTALL_CONF) ./files/add_nut_httpd_conf.default $(1)/etc/uci-defaults/add_nut_httpd_conf $(INSTALL_CONF) $(PKG_INSTALL_DIR)/etc/nut/upsstats.html.sample $(1)/etc/nut/upsstats.html $(INSTALL_CONF) $(PKG_INSTALL_DIR)/etc/nut/upsstats-single.html.sample $(1)/etc/nut/upsstats-single.html - $(INSTALL_CONF) $(PKG_INSTALL_DIR)/etc/nut/upsset.conf.sample $(1)/etc/nut/upsset.conf.disable - $(INSTALL_CONF) $(PKG_INSTALL_DIR)/etc/nut/upsset.conf.sample $(1)/etc/nut/upsset.conf.enable - $(SED) 's/### \?//' $(1)/etc/nut/upsset.conf.enable $(INSTALL_DIR) $(1)/etc/config $(INSTALL_CONF) ./files/nut_cgi $(1)/etc/config/nut_cgi $(INSTALL_DIR) $(1)/etc/init.d @@ -375,7 +382,7 @@ define DriverPackage $(if $(filter $(1),snmp),DEPENDS+= @NUT_DRIVER_SNMP) $(if $(filter $(1),usb),DEPENDS+= @NUT_DRIVER_USB) $(if $(filter $(1),serial),DEPENDS+= @NUT_DRIVER_SERIAL) - $(if $(filter $(1),neon),DEPENDS+= @NUT_DRIVER_NEON) + $(if $(filter $(1),neon),DEPENDS+= @NUT_DRIVER_NEON) endef # Deliberately empty description in order to trigger a build failure. # It should be overridden by the list below, and when updating to a diff --git a/net/nut/files/30-libhid-ups.head b/net/nut/files/30-libhid-ups.head deleted file mode 100755 index eebda998ce2fb..0000000000000 --- a/net/nut/files/30-libhid-ups.head +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/sh - -nut_driver_config() { - local cfg="$1" - local nomatch="$2" - - config_get runas "$cfg" runas "nut" - config_get vendorid "$cfg" vendorid - config_get productid "$cfg" productid - - [ "$ACTION" = "add" ] && [ -n "$DEVNAME" ] && { - chmod 0660 /dev/"$DEVNAME" - chown "${runas:-root}":"$(id -gn "${runas:-root}")" /dev/"$DEVNAME" - } - - if [ "$nomatch" = "1" ]; then - [ "$ACTION" = "add" ] && { - /etc/init.d/nut-server start "$cfg" - } - elif [ "$(printf "%04x" 0x"$pvendid")" = "$vendorid" ] && \ - [ "$(printf "%04x" 0x"$pprodid")" = "$productid" ]; then - [ "$ACTION" = "add" ] && { - /etc/init.d/nut-server start "$cfg" - } - [ "$ACTION" = "remove" ] && { - /etc/init.d/nut-server stop "$cfg" - } - found=1 - fi -} - -perform_libhid_action() { - . /lib/functions.sh - - local vendorid productid runas - local pvendid pprodid found - - pvendid=${PRODUCT%/*} - pvendid=${pvendid%/*} - pprodid=${PRODUCT%/*} - pprodid=${pprodid##*/} - - config_load nut_server - config_foreach nut_driver_config driver 0 - [ "$found" != "1" ] && config_foreach nut_driver_config driver 1 - /etc/init.d/nut-server start upsd -} - -[ -n "$PRODUCT" ] && case "$PRODUCT" in diff --git a/net/nut/files/30-libhid-ups.tail b/net/nut/files/30-libhid-ups.tail deleted file mode 100644 index 3846bc3ee3edf..0000000000000 --- a/net/nut/files/30-libhid-ups.tail +++ /dev/null @@ -1,5 +0,0 @@ -"") - [ -f /var/run/nut/disable-hotplug ] || \ - /etc/init.d/nut-server enabled && perform_libhid_action - ;; -esac diff --git a/net/nut/files/add_nut_httpd_conf b/net/nut/files/add_nut_httpd_conf deleted file mode 100644 index b8fa847f8bf6e..0000000000000 --- a/net/nut/files/add_nut_httpd_conf +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -grep -q '/cgi-bin/nut' /etc/httpd.conf 2>/dev/null || { - echo '/cgi-bin/nut:root:$p$root' >>/etc/httpd.conf - /etc/init.d/uhttpd restart -} diff --git a/net/nut/files/add_nut_httpd_conf.default b/net/nut/files/add_nut_httpd_conf.default new file mode 100644 index 0000000000000..00620c8ac570b --- /dev/null +++ b/net/nut/files/add_nut_httpd_conf.default @@ -0,0 +1,27 @@ +#!/bin/sh + +# UCI default script to set default password to access the NUT CGI pages +# to be the system password for the root user (as with LuCI) + +# $p$root is not an actual password, it is a reference to the password +# for root in the system password (/etc/shadow) password file + +# In recent (relevant) versions of shellcheck busybox is a valid shell type +# shellcheck shell=busybox + +# Only add this default user:password (reference) for NUT CGI if there is +# not an existing password configuration for the NUT CGI, include for other +# users. + +[ -z "$IPKG_ROOT" ] || exit 0 + +touch /etc/httpd.conf +grep -q '^/cgi-bin/nut' /etc/httpd.conf 2>/dev/null || { + # The SC2016 shellcheck directive is required as shellcheck thinks the + # $p$root is a variable that gets expanded + # shellcheck disable=SC2016 + echo '/cgi-bin/nut:root:$p$root' >>/etc/httpd.conf + if ! /etc/init.d/uhttpd restart; then + logger -s -t nut-cgi "Failed to restart uhttpd after http.conf update" + fi +} diff --git a/net/nut/files/libhid-ups.hotplug b/net/nut/files/libhid-ups.hotplug new file mode 100644 index 0000000000000..aad52cf0bee72 --- /dev/null +++ b/net/nut/files/libhid-ups.hotplug @@ -0,0 +1,347 @@ +#!/bin/sh + +# In recent (relevant) versions of shellcheck busybox is a valid shell type +# shellcheck shell=busybox + +# IPKG_INSTROOT is intentionally only set when building an image and +# is intentionally empty on a live OpenWrt device + +# The shellcheck source directives are used during development by external +# tools to find the source files on the development system. They do not +# affect runtime behaviour. + +# Also the filenames in the shellcheck source directives may differ from the +# production names that are sourced on live OpenWrt devices. + +# shellcheck source=net/nut/files/functions.sh.functions +. "${IPKG_INSTROOT}"/lib/functions.sh || { + # Before our sourcing error logging definitions are sourced + logger -s -t nut-usb-hotplug "Unable to source 'functions.sh' in 'libhid-ups' hotplug. Bailing" + exit 1 +} + +# 'shellcheck' complains about nut-common.sh due to a case pattern it does not understand, even +# though it is correctly POSIX compliant +# shellcheck disable=SC1094 +# shellcheck source=net/nut/files/nut-common.sh.functions +. "${IPKG_INSTROOT}"/lib/functions/nut/nut-common.sh || { + # Before our sourcing error logging definitions are sourced + logger -s -t nut-usb-hotplug "Unable to source 'nut-common.sh' in 'libhid-ups' hotplug. Bailing" + exit 1 +} + +# shellcheck source=net/nut/files/nut-service.sh.functions +. "${IPKG_INSTROOT}"/lib/functions/nut/nut-service.sh || { + log_source_error "nut-service.sh" "libhid-ups" nut-usb-hotplug + exit 1 +} + +# shellcheck source=net/nut/files/nut-server-service.sh.functions +. "${IPKG_INSTROOT}"/lib/functions/nut/nut-server-service.sh || { + log_source_error "nut-server-service.sh" "libhid-ups" nut-usb-hotplug + exit 1 +} + +config_load nut_server || { + log_config_load_error "nut_server" "libhid-ups" nut-usb-hotplug + exit 1 +} + +# shellcheck disable=SC2329,SC2317 +start_stop_driver() { + local ups="$1" + local known_devname="$2" + + # ACTION and DEVNAME come from the calling hotplug event + # shellcheck disable=SC2153 + case "$ACTION" in + add) + # Skip hotplug if it is disabled or NUT is shutting down + if ! allow_hotplug_restart; then + logger -s -t nut-usb-hotplug "Hotplug restart disabled starting '$ups' when processing '$ACTION' for '$DEVNAME'" + # This is a configuration not an error + return 0 + fi + + # Set permissions to allow the UPS device to start + # In some cases this may be enough for the UPS to start working + if ! ensure_usb_ups_access "$ups" "$known_devname"; then + logger -s -t nut-usb-hotplug "Unable to enable access to '$ups' when processing '$ACTION' for '$DEVNAME'" + return 0 + fi + + # Start the newly hotplugged UPS + # The initscript is effectively a noop if the UPS is already started + # (does not stop and restart, in that case). + # We check enabled again here, in case of a race condition since + # our last check. + if /etc/init.d/nut-server enabled; then + if ! /etc/init.d/nut-server start "$ups"; then + logger -s -t nut-usb-hotplug "Failed to start '$ups' processing '$ACTION' for '$DEVNAME'" + return 0 + fi + else + logger -s -t nut-usb-hotplug "Hotplug became disabled while processing '$ACTION' for '$DEVNAME'." + return 0 + fi + ;; + remove) + # Stop the newly removed UPS, even if hotplug is disabled. + # We stop only the removed UPS as we want the NUT service and other + # UPS (if any) to remain up. + /etc/init.d/nut-server stop "$ups" || { + logger -s -t nut-usb-hotplug "Failed to stop nut-driver instance (UPS) '$ups'." + return 0 + } + ;; + *) + # Ignore all other hotplug ACTION types + return 0 + ;; + esac + # We do not return 0 here as it is unnecessary (all case branches, including + # catch-all have a return) and shellcheck will complain if we do. +} + +# We do not exit with error as that would abort processing for any other +# drivers/devices + +# shellcheck disable=SC2329,SC2317 +nut_driver_config() { + local ups="$1" + local try_match="$2" + local cfg_vendorid cfg_productid + + # Configuration might not have a vendorid. This is allowed, and in that case + # we keep going as we will restart this UPS with no match. + config_get cfg_vendorid "$ups" vendorid + if [ -n "$cfg_vendorid" ] && ! check_valid_hex_number "${cfg_vendorid/0x/}"; then + logger -s -t nut-usb-hotplug "Configured vendorid for '$ups' is not valid" + nd_driver_config_error=true + return 0 + fi + + case "$cfg_vendorid" in + '') + # Configuration might not have a vendorid. This is allowed, and in that + # case we keep going as we may restart this UPS with no match. + ;; + 0x*) + cfg_vendorid="$(printf "%04x" "$cfg_vendorid")" + ;; + *) + cfg_vendorid="$(printf "%04x" "0x$cfg_vendorid")" + ;; + esac + + # Configuration might not have a productid. This is allowed, and in that case + # we keep going as we will restart this UPS with no match. + config_get cfg_productid "$ups" productid + if [ -n "$cfg_productid" ] && ! check_valid_hex_number "${cfg_productid/0x/}"; then + logger -s -t nut-usb-hotplug "Configured productid for '$ups' is not valid" + nd_driver_config_error=true + return 0 + fi + case "$cfg_productid" in + '') + # Configuration might not have a productid. This is allowed, and in that + # case we keep going as we may restart this UPS with no match. + ;; + 0x*) + cfg_productid="$(printf "%04x" "$cfg_productid")" + ;; + *) + cfg_productid="$(printf "%04x" "0x$cfg_productid")" + ;; + esac + + # ACTION and DEVNAME come from the calling hotplug event + + # If we could not match a device in the previous round, or the configuration + # for the UPS does not have a vendorid and productid, start this UPS + # without checking for a match + if [ "$try_match" = "no" ]; then + if [ "$ACTION" = "add" ]; then + if ! allow_hotplug_restart; then + logger -s -t nut-usb-hotplug "Hotplug restart disabled starting '$ups' when processing '$ACTION' for '$DEVNAME'" + return 0 + fi + # We check enabled again here, in case of a race condition since + # our last check. + if /etc/init.d/nut-server enabled; then + # If we don't have a specific match, attempt a reload rather + # than restart of '$ups'. The nut-server initscript will do a + # start if the reload fails. + /etc/init.d/nut-server reload "$ups" || { + log_error "Failed to reload unmatched driver instance (UPS) '$ups' in nut_driver_config" libhid-ups nut-usb-hotplug + nd_driver_config_error=true + return 0 + } + else + # Initscript should have its own logging, and we do not exit with + # error as that would abort processing other devices + logger -s -t nut-usb-hotplug "NUT server disabled when processing '$ACTION' for '$DEVNAME'." + return 0 + fi + else + # We do not stop running UPS that do not match on vendorid + # and productid on a removal event as that would mean all UPS would + # be stopped if one nomatch UPS were to be removed. This would + # be bad. + return 0 + fi + elif [ -n "$cfg_vendorid" ] && [ -n "$cfg_productid" ] && [ -n "$pvendid" ] && [ -n "$pprodid" ]; then + if ! check_valid_hex_number "$pvendid"; then + logger -s -t nut-usb-hotplug "Hotplug vendor id for '$ups' is not valid" + nd_driver_config_error=true + return 0 + fi + if ! check_valid_hex_number "$pprodid"; then + logger -s -t nut-usb-hotplug "Hotplug product id for '$ups' is not valid" + nd_driver_config_error=true + return 0 + fi + if [ "$(printf "%04x" 0x"$pvendid")" = "$cfg_vendorid" ] && + [ "$(printf "%04x" 0x"$pprodid")" = "$cfg_productid" ]; then + # Round 1: If we have a match for hotplug event and the UCI config + # start this UPS and record that fact, otherwise skip this UCI UPS + # for this round + # Initscript should have its own logging + if ! start_stop_driver "$ups" "/dev/$DEVNAME"; then + log_error "Error starting matched driver instance (UPS) in nut_driver_config" libhid-ups nut-usb-hotplug + nd_driver_config_error=true + return 0 + fi + # shellcheck disable=SC2317 + nd_found=true + return 0 + fi + fi + # We do not return 0 here as it is unnecessary (all if branches have a + # return) and shellcheck will complain if we do. +} + +perform_libhid_action() { + # Variable to indicate NUT driver instance (UPS) section found + local nd_found=false + local nd_driver_config_error=false + local pvendid pprodid + + # Only find statepath and runas once per event + # defines STATEPATH + find_statepath "upsd" "nut_server" || { + logger -s -t nut-usb-hotplug "Failed to set STATEPATH" + return 1 + } + # sets RUNAS + find_runas "upsd" "nut_server" || { + logger -s -t nut-usb-hotplug "Failed to set RUNAS" + return 1 + } + + # PRODUCT comes from the calling hotplug event, and + # is of the form vendorid/productid/other + # The code below uses shell parameter expansion to split out + # vendorid as pvendid and productid as pprodid + + # We disable the check for possible misspelling as the variables names + # are similar, but PRODUCT, pvendid, and pprodid are correct. + # shellcheck disable=SC2153 + + # PRODUCT from hotplug has the form aaaa/bbbb/cccc, where aaaa, bbbb, + # cccc are 1-4 hex digits. + + # From PRODUCT, strip final / and everything after it (e.g. /cccc) + pvendid=${PRODUCT%/*} + # pvendid is now of form aaaa/bbbb + # Strip final / and anything after (e.g. /bbbb), leaving aaaa in pvendid + pvendid=${pvendid%/*} + # From PRODUCT, strip final / and everything after it (e.g. /cccc) + pprodid=${PRODUCT%/*} + # pprodid is now of form aaaa/bbbb + # Strip everything before and including the remaining / (e.g. aaaa/) + # This leaves bbbb in pprodid + pprodid=${pprodid##*/} + + # Attempt to find and (re)start a UPS with a match for the + # pvendid and pprodid we have parsed. + config_foreach nut_driver_config driver yes + + [ "$nd_driver_config_error" = "false" ] || return 1 + # Only if we cannot find a matching UPS (driver instance) configuration + # do we try again, this time restarting all UPS (driver) instances + # This is for the event the device matching configuration for NUT does + # not use USB vendorid and productid (and possibly serial number), which + # is all we have to work with in this script (and sourced scripts) + if [ "$nd_found" = "false" ]; then + config_foreach nut_driver_config driver no + fi + + [ "$nd_driver_config_error" = "false" ] || return 1 + # Calls nut-server start for upsd, which has checks for the existence of a + # ups driver and upsd configuration and will not start if configuration + # is missing. In addition any UPS for which configuration has been removed + # and not already stopped, will be stopped. This is preferable to doing + # nothing on removals. + if ! allow_hotplug_restart; then + logger -s -t nut-usb-hotplug "Hotplug restart disabled when processing '$ACTION' for '$DEVNAME'" + return 0 + fi + # We check enabled again here, in case of a race condition since + # our last check. + if /etc/init.d/nut-server enabled; then + # Initscript should have its own logging + if ! /etc/init.d/nut-server start upsd; then + return 1 + fi + return 0 + else + logger -s -t nut-usb-hotplug "NUT server disabled when processing '$ACTION' for '$DEVNAME'." + return 0 + fi + # We do not return 0 here as it is unnecessary (all if branches have a + # return) and shellcheck will complain if we do. +} + +# PRODUCT comes from the calling hotplug event, and +# is of the form vendorid/productid/other + +# This is an incomplete script which gets filled by libhid-ups.parsed-usermap. +# This confuses shellcheck and reviewers, hence the disables below. +# shellcheck disable=SC1073,SC1072 +case "$PRODUCT" in +### insert libhid-ups.parsed-usermap content here ### +# The result is cases of the form +# "vendorid/productid/other" ) | \ +# to be matched against the PRODUCT from the calling hotplug event +# and does NOT use ';;' (so falls through to code below). +# The empty string matches when PRODUCT is empty, but does not match +# when PRODUCT is neither empty nor one of the case targets, above + +"") + # Skip hotplug actions if NUT hotplug is disabled + # Uses NUT_DISABLE_HOTPLUG_PATH, defined in nut-service.sh and sourced + # NUT_DISABLE_HOTPLUG_PATH is an external sentinel used to indicate NUT + # hotplug is disabled. + if ! allow_hotplug_restart; then + logger -s -t nut-usb-hotplug "Hotplug restart disabled when processing '$ACTION' for '$DEVNAME'" + exit 0 + fi + # We check enabled again here, in case of a race condition since + # our last check. + if ! /etc/init.d/nut-server enabled; then + logger -s -t nut-usb-hotplug "NUT server disabled when processing '$ACTION' for '$DEVNAME'." + exit 0 + fi + if ! perform_libhid_action; then + exit 1 + fi + exit 0 + ;; +*) + # We intentionally do nothing when PRODUCT is neither empty, nor matches + # the libhid-ups.parsed-usermap content cases. In that case the PRODUCT is + # for a device other than a UPS supported by NUT's libhid-ups. + exit 0 + ;; +esac diff --git a/net/nut/files/nut-cgi.init b/net/nut/files/nut-cgi.init old mode 100755 new mode 100644 index e5c3cf193dd87..9c95923582fbe --- a/net/nut/files/nut-cgi.init +++ b/net/nut/files/nut-cgi.init @@ -1,76 +1,341 @@ #!/bin/sh /etc/rc.common # Copyright © 2012-2026 OpenWrt.org -# + +# In recent (relevant) versions of shellcheck busybox is a valid shell type +# shellcheck shell=busybox + # This is free software, licensed under the GNU General Public License v2. # See /LICENSE for more information. # + +# shellcheck disable=SC2034 START=87 STOP=23 USE_PROCD=1 -DEFAULT=/etc/default/nut -UPSCGI_C=/var/etc/nut/hosts.conf -UPSCGI_S=/var/etc/nut/upsset.conf +# Default is hardcoded in other program's configuration, as well as compile +# time options for NUT CGI executable +# /var is typically a tmpfs on OpenWrt and is not persisted across reboots +UPSCGI_CONF_DIR=/var/etc/nut + +# Ephemeral configuration for configuring NUT hosts using CGI and upsset +# (persisted using UCI) +UPSCGI_UPSSET_CONF="${UPSCGI_CONF_DIR}/upsset.conf" +# Ephemeral configuration for NUT hosts to display using CGI (persisted +# using UCI) +UPSCGI_HOSTS_CONF="${UPSCGI_CONF_DIR}/hosts.conf" + +# IPKG_INSTROOT is intentionally only set when building an image and +# is intentionally empty on a live OpenWrt device + +# Shellcheck source paths intentionally point to the location of files of +# the scripts in the development environment (where shellcheck is used), not +# on the live OpenWrt device + +# shellcheck source=net/nut/files/functions.sh.functions +. "${IPKG_INSTROOT}"/lib/functions.sh || { + # Before our sourcing error logging definitions are sourced + logger -s -t nut-cgi "Unable to source 'functions.sh'" + exit 1 +} + +# 'shellcheck' complains about nut-common.sh due to a case pattern it does not understand, even +# though it is correctly POSIX compliant +# shellcheck disable=SC1094 +# shellcheck source=net/nut/files/nut-common.sh.functions +. "${IPKG_INSTROOT}"/lib/functions/nut/nut-common.sh || { + # Before our sourcing error logging definitions are sourced + logger -s -t nut-cgi "Failed to load nut-common.sh" + exit 1 +} + +restore_umask_on_exit + +ensure_conf_dir_exists() { + # If the directory already exists we presume the creator of the directory + # has set the correct permissions. + if [ ! -d "$UPSCGI_CONF_DIR" ]; then + # Make directory for the UPS CGI configuration files, if not present + # Set permission on dir we create for UPS CGI configuration files + # All NUT configuration files are here, some which must be readable + # by other binaries with different group and ownership (therefore must + # be world readable) + # NOTE: Ensure sensitive files have more restrictive permissions + umask 022 + if ! mkdir -p "$UPSCGI_CONF_DIR"; then + restore_umask + return 1 + fi + restore_umask + fi + return 0 +} + +set_upsset_conf_content() { + local upsset_conf_content="$1" + + umask 133 + if ! printf "%s" "$upsset_conf_content" >"$UPSCGI_UPSSET_CONF"; then + restore_umask + log_error "failed to configure upsset; disabling NUT CGI" nut-cgi nut-cgi + return 1 + fi + restore_umask + return 0 +} + +disable_upsset() { + ensure_conf_dir_exists || { + log_error "failed to disable upsset due to invalid directory; disabling NUT CGI" nut-cgi nut-cgi + return 1 + } + # Use embedded content in preference to previous symlink method to avoid an + # attack vector via modified symlink target + # Prefer variable to heredoc due to complication with if, then around a + # heredoc when using shellcheck and shfmt together. + set_upsset_conf_content "# Network UPS Tools - upsset.conf sample file for OpenWrt +# +# This file is provided to ensure that you do not expose your upsd server +# to the world upon installing the CGI programs. Specifically, it keeps +# the upsset.cgi program from running until you have assured it that you +# have secured your web server's CGI directory. +# +# NOTE: Contents of this file should be pure ASCII (character codes +# not in range would be ignored with a warning message). +# +### +### I_HAVE_SECURED_MY_CGI_DIRECTORY +### +" || return 1 + return 0 +} -nut_upscgi_upsset() { +configure_nut_upscgi_upsset() { local cfg="$1" local enable config_get_bool enable "$cfg" enable 0 - if [ "$enable" -eq 1 ]; then - ln -sf /etc/nut/upsset.conf.enable "$UPSCGI_S" + # Use embedded content in preference to previous symlink method to avoid an + # attack vector via modified symlink target + # Prefer variable to heredoc due to complication with if..then around a + # heredoc when using shellcheck and shfmt together. + [ "$enable" = "1" ] || { + # Logs an error in the disable_upsset function call if it fails + disable_upsset || return 1 + # not enabled (disabled) is a valid configuration choice + return 0 + } + if ensure_conf_dir_exists; then + # Leading newline is intentional + set_upsset_conf_content " +I_HAVE_SECURED_MY_CGI_DIRECTORY +" || { + upscgi_upsset_failed="true" + return 1 + } + return 0 else - ln -sf /etc/nut/upsset.conf.disable "$UPSCGI_S" + upscgi_upsset_failed="true" + # Logs an error in the disable_upsset function call if it fails + disable_upsset + return 1 fi + # No return 0 here as it would be dead code, and shellcheck would complain + # about the dead code. } -nut_upscgi_add() { - local cfg="$1" - local upsname - local hostname - local port - local displayname - - config_get upsname "$cfg" upsname - config_get hostname "$cfg" hostname localhost - config_get port "$cfg" port - config_get pass "$cfg" password - system="$upsname@$hostname" +log_validation_error() { + local var="$1" + local ups="$2" + log_error "Unsafe characters in '$var' for '$ups'; skipping" nut-cgi nut-cgi +} + +validate_host() { + local ups="$1" + local upsname="$2" + local hostname="$3" + local port="$4" + local displayname="$5" + + # We do not unset upscgi_hosts_exists on error as we do not want to block + # monitoring hosts with the needed configuration as a result of those that + # do not + for var in upsname hostname port displayname; do + case "$var" in + upsname) + if ! check_safe_uci_name "$upsname"; then + log_validation_error "$var" "$ups" + return 1 + fi + ;; + hostname) + case "$hostname" in + "" | *[!a-zA-Z0-9.:-\[\]-]*) + log_validation_error "$var" "$ups" + return 1 + ;; + esac + ;; + displayname) + # We accept a minimally useful set of characters for a display name + # We specifically omit characters like $ % " and ' + # Some AI code reviewers get confused by \(\) and \space in the case + # pattern, but this is correct for busybox ash in modern OpenWrt + # and does not allow \ through the validation. + case "$displayname" in + *[!a-zA-Z0-9+!\(\)\ _=:.,-]*) + log_validation_error "$var" "$ups" + return 1 + ;; + esac + ;; + port) + # We don't log missing port log, as port is optional (NUT defaults + # to using 3493 when it is not present) + if [ -n "$port" ]; then + case "$port" in + 0*) + log_validation_error "$var" "$ups" + return 1 + ;; + *[!0-9]*) + log_validation_error "$var" "$ups" + return 1 + ;; + esac + # Catches illegal port numbers, including negative port numbers + if [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then + log_error "Port set, but not between 1-65535 for '$ups'" nut-cgi nut-cgi + return 1 + fi + fi + ;; + esac + done + return 0 +} + +# Must be called from service_reload +nut_upscgi_add_host() { + local ups="$1" + local upsname hostname port system displayname + + config_get upsname "$ups" upsname "$ups" + config_get hostname "$ups" hostname localhost + config_get port "$ups" port + config_get displayname "$ups" displayname "$ups" + + # We do not error exit as that would abort processing of other hosts + validate_host "$ups" "$upsname" "$hostname" "$port" "$displayname" || return + + system="${upsname}@$hostname" if [ -n "$port" ]; then - system="$system:$port"; + system="$system:$port" + fi + + if ! printf "%s\n" "MONITOR $system \"$displayname\"" >>"${UPSCGI_HOSTS_CONF}.new"; then + log_error "Failed to add MONITOR to CGI host.conf for '$ups'" nut-cgi nut-cgi + # Failed printf could result in inconsistent UPSCGI_HOSTS_CONF.new state, so recreate it as + # empty so later hosts can still potentially be properly configured + if rm -f "${UPSCGI_HOSTS_CONF}.new"; then + return + else + # Cascade failure. Bail. + log_error_exit "Cascade failure in nut_upscgi_add_host. Bailing." nut-cgi nut-cgi + fi fi - config_get displayname "$cfg" displayname - echo "MONITOR $system \"$displayname\"" >> "$UPSCGI_C" + upscgi_hosts_exists="true" + return 0 } service_reload() { - mkdir -m 0755 -p "$(dirname "$UPSCGI_C")" - rm -f "$UPSCGI_C" - rm -f "$UPSCGI_S" + # Callers should not need to see the variables below outside this function + # Conversely, functions called by this function are able to see and set + # the variables below, which act as 'pseudo-globals' to called functions. + local upscgi_hosts_exists="false" + local upscgi_upsset_failed="false" + + config_load nut_cgi || { + log_config_load_error "nut_cgi" "nut-cgi" "nut-cgi" + exit 1 + } + + # NUT configuration does not exist if NUT CGI cannot be configured, so + # stop service and bailout. + ensure_conf_dir_exists || { + service_stop + return 1 + } - config_load nut_cgi + rm -f "${UPSCGI_HOSTS_CONF}.new" + umask 133 + touch "${UPSCGI_HOSTS_CONF}.new" || { + restore_umask + service_stop + return 1 + } + restore_umask - config_foreach nut_upscgi_add host - config_foreach nut_upscgi_upsset upsset + # We do not exit on error here as we do not want to block monitoring hosts + # with the needed configuration as a result of those that do not + config_foreach nut_upscgi_add_host host - [ -s "$UPSCGI_C" ] && chmod 640 "$UPSCGI_C" + # If new host configuration was successfully created + if [ "$upscgi_hosts_exists" = "true" ] && [ -s "${UPSCGI_HOSTS_CONF}.new" ]; then + # Move the new configuration file to active use, + # removing the old configuration in the process + mv -f "${UPSCGI_HOSTS_CONF}.new" "${UPSCGI_HOSTS_CONF}" + elif [ "$upscgi_hosts_exists" = "true" ]; then + rm -f "${UPSCGI_HOSTS_CONF}.new" + else + # If no hosts are configured, use an empty hosts.conf (no configuration) + rm -f "${UPSCGI_HOSTS_CONF}" + rm -f "${UPSCGI_HOSTS_CONF}.new" + if ! touch "${UPSCGI_HOSTS_CONF}"; then + log_error "Failed to create empty hosts.conf" nut-cgi nut-cgi + return 1 + fi + # Empty hosts is not necessarily an error, but we log for information + logger -s -t nut-cgi "No configured hosts" + fi + + # NUT CGI host file configuration is independent of upsset configuration + config_foreach configure_nut_upscgi_upsset upsset + if [ "$upscgi_upsset_failed" = "true" ]; then + disable_upsset + return 1 + fi + return 0 } start_service() { - service_reload + service_reload || return 1 + return 0 } reload_service() { - service_reload + service_reload || { + stop_service + return 1 + } + return 0 } stop_service() { - rm -f "$UPSCGI_C" - rm -f "$UPSCGI_S" - ln -sf /etc/nut/upsset.conf.disable "$UPSCGI_S" + # Remove the hosts configuration file + # Since this is only a configuration generation service, the only way to + # stop serving (without stopping the web server for all uses) is to remove + # the NUT CGI configuration + rm -f "$UPSCGI_HOSTS_CONF" + rm -f "$UPSCGI_HOSTS_CONF.new" + + disable_upsset || return 1 + return 0 } service_triggers() { - procd_add_reload_trigger "nut_cgi" + # Add a reload trigger on changes to the nut_cgi UCI config + procd_add_reload_trigger "nut_cgi" || return 1 + return 0 } diff --git a/net/nut/files/nut-common.sh.functions b/net/nut/files/nut-common.sh.functions new file mode 100644 index 0000000000000..1804eb34db73f --- /dev/null +++ b/net/nut/files/nut-common.sh.functions @@ -0,0 +1,193 @@ +#!/bin/sh +# Shebang line included so editors and shellcheck/shfmt know this contains +# shell code + +# Common helper functions for NUT server initscript + +# In recent (relevant) versions of shellcheck busybox is a valid shell type +# shellcheck shell=busybox + +# Pre-requisite sourcing for functions this script uses +# * /lib/functions.sh has been sourced + +# We use explicit return 0 at the end of functions throughout, except where they +# would be dead code, for clarity + +# Preserve current umask in CUR_UMASK, unless a umask was previously captured +CUR_UMASK="${CUR_UMASK:-$(umask)}" + +restore_umask() { + umask "$CUR_UMASK" +} + +restore_umask_on_exit() { + # Restore umask on exit + # shellcheck disable=SC3047 + trap restore_umask EXIT +} + +# Check string to contain only characters allowed in an UCI section name +check_safe_uci_name() { + local val="$1" + [ -n "$val" ] || return 1 + case "$val" in + *[!a-zA-Z0-9_-]*) + return 1 + ;; + esac + return 0 +} + +# Check string to contain only characters valid in a RFC822 Date header +check_safe_date() { + local in_date="$1" + [ -n "$in_date" ] || return 1 + # Some AI code reviewers get confused by \(\) and \space in the case + # pattern, but this is correct for busybox ash in modern OpenWrt + # and does not allow \ through the validation. + # We want to allow space, comma, and hyphen as they can all appear in an + # RFC822 Date: header + case "$in_date" in + *[!a-zA-Z0-9_:+\(\),\ -]*) + return 1 + ;; + esac + return 0 +} + +# Check if input is a valid user or group name +check_valid_user_group_name() { + local in_name="$1" + case "$in_name" in + [a-zA-Z_][a-zA-Z0-9_-]*) ;; + # Includes the case of an empty string + *) + return 1 + ;; + esac + return 0 +} + +# shellcheck disable=SC2329,SC2317 +check_valid_hex_number() { + local number="$1" + local max_length="$2" + [ -n "$max_length" ] || max_length=4 + case "$number" in + '') + # Empty string is not valid + return 1 + ;; + 0x*) + case "$number" in + 0x[!0-9a-fA-F]*) + return 1 + ;; + 0x[0-9a-fA-F]*) + # Minimum of 1 hex digit + # Has a maximum of 4 hex digits (1-max_length hex digits allowed) + if [ "${#number}" -gt "$((max_length + 2))" ]; then + return 1 + fi + return 0 + ;; + *) + # Redundant, but add clarity for reviewers, especially automated + # ones that can get confused without it. + return 1 + ;; + esac + ;; + *[!0-9a-fA-F]*) + return 1 + ;; + *) + # Has a maximum of 4 hex digits (1-max_length hex digits allowed) + if [ "${#number}" -gt "$max_length" ]; then + return 1 + fi + return 0 + ;; + esac + # We do not return 0 here as it is unnecessary (all case branches, including + # catch-all have a return) and shellcheck will complain if we do. +} + +# Wrap logging, in case we decide to update the logging, everywhere +log_msg() { + local reason="$1" + local current_script="$2" + local syslog_id="$3" + local level="${4:-notice}" + local facility="${5:-daemon}" + + logger -s -t "$syslog_id" -p "${facility}.${level}" "'$reason' in '$current_script'" || { + # We use 'tr' as changing case via variable parameter substitution is not available in ash on OpenWrt + # shellcheck disable=SC2018,SC2019 + level="$(echo "$level" | tr 'a-z' 'A-Z')" + printf "'%s': Logging failed during $level: '%s' in '%s'\n" "$syslog_id" "$reason" "$current_script" >&2 + } + case "$level" in + crit) + exit 1 + ;; + err | error | warn) + return 1 + ;; + *) + return 0 + ;; + esac +} + +log_source_error() { + local sourced_script="$1" + local current_script="$2" + local syslog_id="$3" + log_msg "Unable to source '$sourced_script'" "$current_script" "$syslog_id" "crit" +} + +log_config_load_error() { + local config="$1" + local current_script="$2" + local syslog_id="$3" + + log_msg "Unable to load configuration '$config'" "$current_script" "$syslog_id" "crit" +} + +# Wrap logging of errors, in case we decide to update the logging, everywhere +# we do error logging with script exit +log_error_exit() { + local reason="$1" + local current_script="$2" + local syslog_id="$3" + + log_msg "$reason" "$current_script" "$syslog_id" "crit" +} + +# Wrap logging of errors, in case we decide to update the logging, everywhere +# we do error logging without hard exit +log_error() { + local reason="$1" + local current_script="$2" + local syslog_id="$3" + + log_msg "$reason" "$current_script" "$syslog_id" "error" +} + +note_section_of_type() { + have_expected_section="true" +} + +have_section_of_type() { + local section_type="$1" + + # 'pseudo-global' to capture result from config_foreach + local have_expected_section="false" + config_foreach note_section_of_type "$section_type" + + if [ "$have_expected_section" = "true" ]; then + return 0 + fi + return 1 +} diff --git a/net/nut/files/nut-monitor-config.sh.functions b/net/nut/files/nut-monitor-config.sh.functions new file mode 100644 index 0000000000000..e51f945ad67b0 --- /dev/null +++ b/net/nut/files/nut-monitor-config.sh.functions @@ -0,0 +1,339 @@ +#!/bin/sh +# Shebang line included so editors and shellcheck/shfmt know this contains +# shell code + +# Helper functions for NUT monitor (upsmon) configuration handling + +# In recent (relevant) versions of shellcheck busybox is a valid shell type +# shellcheck shell=busybox + +# Pre-requisite sourcing for functions this script uses +# * /lib/functions has been sourced +# * /lib/functions/nut/nut-common.sh has been sourced + +# Upstream NUT event type names (in the order listed in +# https://networkupstools.org/docs/man/upsmon.conf.html) +# NUT_EVENT_TYPES is used later without quotes for intentional word-splitting +# Globbing is disabled in that case +NUT_EVENT_TYPES="ONLINE ONBATT LOWBATT FSD COMMOK COMMBAD SHUTDOWN REPLBATT" +append NUT_EVENT_TYPES "NOCOMM NOPARENT CAL NOTCAL OFF NOTOFF BYPASS NOTBYPASS" +append NUT_EVENT_TYPES "ECO NOTECO OVER NOTOVER TRIM NOTTRIM BOOST NOTBOOST" +append NUT_EVENT_TYPES "OTHER NOTOTHER SUSPEND_STARTING SUSPEND_FINISHED" + +# Upstream NUT upsmon.conf option names (in the order listed in +# https://networkupstools.org/docs/man/upsmon.conf.html) +# except MONITOR, NOTIFYMSG, NOTIFYFLAG, and RUN_AS_USER which are handled +# separately +NUT_UPSMON_OPTIONS="DEADTIME FINALDELAY HOSTSYNC MINSUPPLIES NOCOMMWARNTIME" +append NUT_UPSMON_OPTIONS "POLLFAIL_LOG_THROTTLE_MAX NOTIFYCMD POLLFREQ" +append NUT_UPSMON_OPTIONS "POLLFREQALERT POWERDOWNFLAG OFFDURATION OVERDURATION" +append NUT_UPSMON_OPTIONS "OBLBDURATION RBWARNTIME SHUTDOWNCMD SHUTDOWNEXIT" +append NUT_UPSMON_OPTIONS "CERTPATH CERTFILE CERTIDENT CERTHOST DEBUG_MIN" + +NUT_UPSMON_BOOL_OPTIONS="ALARMCRITICAL CERTVERIFY FORCESSL" + +# Location of NUT's UPS monitoring client (upsmon) configuration +UPSMON_C=/var/etc/nut/upsmon.conf +# Location of NUT's mode configuration +NUT_CONF=/var/etc/nut/nut.conf +# Path to PID file for upsmon +# shellcheck disable=SC2034 +PIDFILE=/var/run/upsmon.pid + +# config_load is done by the sourcing script, before executing any functions in +# this file + +# Get notification configuration +# In nut_monitor UCI configuration a 'notification'-type config section must +# be named with the name of a NUT_EVENT_TYPE. This NUT_EVENT_TYPE maps to +# a notification event type emitted by upsmon. +nut_get_notifications() { + local event="$1" + local defaultnotify="$2" + local config_file="$3" + local event_types val + + # Try to remove the name of the UCI section surrounded by spaces. + # If this differs from the NUT_EVENT_TYPE contents, then the section + # is a valid NUT event type, so use it. + event_types=" $NUT_EVENT_TYPES " + if [ "${event_types#*" $event "}" != "${event_types}" ]; then + config_get val "$event" message + if [ -n "$val" ]; then + if ! printf 'NOTIFYMSG %s %s\n' "$event" "$val" >>"$config_file"; then + log_error "upsmon section '$event' failed to write 'NOTIFYMSG' for '$event'" nut-monitor-config.sh nut-monitor-config + return 1 + fi + fi + config_get val "$event" flag "$defaultnotify" + if [ -n "$val" ]; then + if ! printf 'NOTIFYFLAG %s %s\n' "$event" "$val" >>"$config_file"; then + log_error "upsmon section '$event' failed to write 'NOTIFYFLAG' for '$event'" nut-monitor-config.sh nut-monitor-config + return 1 + fi + event_notify_flags_not_found="${event_notify_flags_not_found/$event/}" + fi + else + log_error "$event is not a valid NUT message event type" nut-monitor-config.sh nut-monitor-config + return 1 + fi +} + +upsmon_conf_get_write() { + local cfg="$1" + local config_file="$2" + local uci_option="$3" + local nut_option="$4" + local is_bool="$5" # optional parameter + local default="$6" + local val + + if [ -n "$is_bool" ] && [ "$is_bool" = "true" ]; then + # Will always get a value - either 0 or 1 + config_get_bool val "$cfg" "$uci_option" "$default" + else + config_get val "$cfg" "$uci_option" "$default" + fi + + if [ -n "$val" ]; then + printf "%s %s\n" "$nut_option" "$val" >>"$config_file" || { + log_error "upsmon section '$cfg' failed to write '$nut_option'" nut-monitor-config.sh nut-monitor-config + return 1 + } + return 0 + else + # Will never trigger for a bool + return 2 + fi +} + +# Generate upsmon.conf +nut_upsmon_conf() { + local config_file="$1" + local cfg="upsmon" + local val defaultnotify nut_option uci_option conf_ret + local event_notify_flags_not_found upsmon_bool_options nut_bool + upsmon_bool_options=" $NUT_UPSMON_BOOL_OPTIONS " + + # Note that we use '-u $RUNAS' on the daemon command line in preference to + # the RUN_AS_USER configuration in "$config_file" + + # Word-splitting is intentional here, and we know NUT_UPSMON_OPTIONS and + # NUT_UPSMON_BOOL_OPTIONS are safe because we define them. + set -f + for nut_option in $NUT_UPSMON_OPTIONS $NUT_UPSMON_BOOL_OPTIONS; do + nut_bool="false" + # The nut_option and corresponding uci_option are known to be + # pure 7-bit ASCII + # We use tr because ash does not support *global* replacement using + # parameter expansion + # shellcheck disable=SC2019,SC2018 + uci_option="$(echo "$nut_option" | tr 'A-Z_' 'a-z')" + case "$nut_option" in + DEBUG_MIN) + config_get val "$cfg" "$uci_option" + case "$val" in + '') ;; + *[!0-9]*) + log_error "upsmon section '$cfg' bad value for 'debugmin'" nut-monitor-config.sh nut-monitor-config + return 1 + ;; + *) + if ! printf "%s %s\n" "$nut_option" "$val" >>"$config_file"; then + log_error "upsmon section '$cfg' failed to write 'DEBUG_MIN'" nut-monitor-config.sh nut-monitor-config + return 1 + fi + ;; + esac + ;; + HOSTSYNC) + # also try hotsync uci_option (typo in previous config handler), preferring hostsync + upsmon_conf_get_write "$cfg" "$config_file" "$uci_option" "$nut_option" + conf_ret=$? + case "$conf_ret" in + 1) + return 1 + ;; + 2) + upsmon_conf_get_write "$cfg" "$config_file" "hotsync" "$nut_option" + conf_ret=$? + if [ "$conf_ret" -eq 1 ]; then + return 1 + fi + ;; + esac + ;; + SHUTDOWNCMD) + upsmon_conf_get_write "$cfg" "$config_file" "$uci_option" "$nut_option" + conf_ret=$? + case "$conf_ret" in + 1) + return 1 + ;; + 2) + if ! printf "%s %s\n" "$nut_option" "/usr/sbin/nutshutdown" >>"$config_file"; then + log_error "upsmon section '$cfg' failed to write 'SHUTDOWNCMD'" nut-monitor-config.sh nut-monitor-config + return 1 + fi + ;; + esac + ;; + ALARMCRITICAL) + upsmon_conf_get_write "$cfg" "$config_file" "$uci_option" "$nut_option" "true" "1" + conf_ret=$? + if [ "$conf_ret" -eq 1 ]; then + return 1 + fi + ;; + POWERDOWNFLAG) + upsmon_conf_get_write "$cfg" "$config_file" "$uci_option" "$nut_option" "false" "$NUT_KILLPOWER" + conf_ret=$? + if [ "$conf_ret" -eq 1 ]; then + return 1 + fi + ;; + *) + if [ "${upsmon_bool_options/$nut_option/}" != "${upsmon_bool_options}" ]; then + nut_bool="true" + fi + upsmon_conf_get_write "$cfg" "$config_file" "$uci_option" "$nut_option" "$nut_bool" + conf_ret=$? + if [ "$conf_ret" -eq 1 ]; then + return 1 + fi + ;; + esac + done + set +f + + event_notify_flags_not_found="$NUT_EVENT_TYPES" + config_get val "$cfg" defaultnotify "SYSLOG" + defaultnotify="$val" + config_foreach nut_get_notifications notifications "$val" "$config_file" + + # Otherwise the default is WALL+SYSLOG + event_notify_flags_not_found="$(printf "%s" "$event_notify_flags_not_found" | tr -s ' ')" + # We intentionally word-split on event_notify_flags-not-found to iterate + # over event types. + set -f + for event in $event_notify_flags_not_found; do + if ! printf "NOTIFYFLAG %s %s\n" "$event" "$defaultnotify" >>"$config_file"; then + log_error "upsmon section '$cfg' failed to write 'NOTIFYFLAG' for '$event'" nut-monitor-config.sh nut-monitor-config + return 1 + fi + done + set +f + return 0 +} + +nut_upsmon_add() { + local cfg="$1" + local config_file="$2" + local upsname + local hostname + local port + local powervalue + local username + local password + local system + local type + + config_get upsname "$cfg" upsname "$cfg" + config_get hostname "$cfg" hostname localhost + config_get port "$cfg" port + config_get powervalue "$cfg" powervalue 1 + config_get username "$cfg" username + config_get password "$cfg" password + config_get type "$cfg" type secondary + + system="$upsname@$hostname" + if [ -n "$port" ]; then + system="$system:$port" + fi + + if [ -z "$username" ] || [ -z "$password" ]; then + log_error "upsmon section '$cfg' missing value(s) for MONITOR line" nut-monitor-config.sh nut-monitor-config + upsmon_conf_fail="true" + else + # Write MONITOR line (including password) to config_file (upsmon.conf) + if ! printf "MONITOR %s %s %s %s %s\n" "$system" "$powervalue" "$username" "$password" "$type" >>"$config_file"; then + log_error "upsmon section '$cfg' failed to write MONITOR line for '$system'" nut-monitor-config.sh nut-monitor-config + upsmon_conf_fail="true" + else + have_monitor_line="true" + fi + fi +} + +build_config() { + local conf_group + local upsmon_conf_fail="false" + local have_monitor_line="false" + + conf_group="$(id -gn "${RUNAS:-nutmon}")" + + if [ -z "$conf_group" ]; then + log_error "upsmon build_config failed to find group for RUNAS user" nut-monitor-config.sh nut-monitor-config + upsmon_conf_fail="true" + else + # This directory is shared with the nut-server which run as as a + # different user and group, so must be all readable. We set the + # ownership and permissions on individual files more restrictively, as + # needed. + # shellcheck disable=SC2174 + umask 022 + mkdir -p "$(dirname "$UPSMON_C")" + + umask 127 + touch "$UPSMON_C.new" + chgrp "$conf_group" "$UPSMON_C.new" + printf "%s\n" "# Config file automatically generated from UCI config" >>"$UPSMON_C.new" + + if nut_upsmon_conf "$UPSMON_C.new"; then + # upsmon_conf_fail will be set in this function's context by nut_upsmon_add, on error + config_foreach nut_upsmon_add monitor "$UPSMON_C.new" + if [ "$upsmon_conf_fail" = "true" ]; then + log_error "'monitor' type sections must be correctly configured" nut-monitor-config.sh nut-monitor-config + return 1 + fi + if [ "$have_monitor_line" = "false" ]; then + log_msg "Must have at least one 'monitor' type section" nut-monitor-config.sh nut-monitor-config warn + return 1 + fi + else + log_error "upsmon section name 'upsmon' not correctly configured" nut-monitor-config.sh nut-monitor-config + return 1 + fi + fi + + # In the event of configuration failure, stop the + # service and remove the ephemeral configuration files + if [ "$upsmon_conf_fail" = "true" ]; then + # If we no longer have configuration, stop the service + return 1 + else + # Atomically make the new config the active config + mv -f "$UPSMON_C.new" "$UPSMON_C" || return 1 + fi + + # Failure to write nut.conf is not hard-fatal although it means the NUT will + # not start the service. + # Also, we only write nut.conf if there is not one already + if [ ! -s "$NUT_CONF" ]; then + umask 133 + if ! printf "MODE=netclient\n" >"$NUT_CONF"; then + log_error "upsmon creation of nut.conf failed" nut-monitor-config.sh nut-monitor-config + return 1 + fi + else + # Otherwise if nut-server is already configured, make sure both + # nut-server and nut-monitor (this service) are started + if grep -q 'MODE=netserver' "$NUT_CONF"; then + # In modern OpenWrt 'sed -i' modifies the specified files, without backup + sed -i -e 's/netserver/both/' "$NUT_CONF" || { + log_error "Failed to update nut.conf to support both upsmon and upsd" nut-monitor-config.sh nut-monitor-config + } + fi + fi + return 0 +} diff --git a/net/nut/files/nut-monitor.init b/net/nut/files/nut-monitor.init index cd6a460141d4f..4f3690e71ea38 100644 --- a/net/nut/files/nut-monitor.init +++ b/net/nut/files/nut-monitor.init @@ -1,286 +1,198 @@ #!/bin/sh /etc/rc.common -# shellcheck shell=ash +# In recent (relevant) versions of shellcheck busybox is a valid shell type +# shellcheck shell=busybox # shellcheck disable=SC2034 START=82 STOP=28 USE_PROCD=1 -UPSMON_C=/var/etc/nut/upsmon.conf -PIDFILE=/var/run/upsmon.pid -# Upstream NUT event type names (in the order listed in -# https://networkupstools.org/docs/man/upsmon.conf.html) -NUT_EVENT_TYPES=" ONLINE ONBATT LOWBATT FSD COMMOK COMMBAD SHUTDOWN REPLBATT NOCOMM NOPARENT CAL NOTCAL OFF NOTOFF BYPASS NOTBYPASS ECO NOTECO OVER NOTOVER TRIM NOTTRIM BOOST NOTBOOST OTHER NOTOTHER SUSPEND_STARTING SUSPEND_FINISHED " +# IPKG_INSTROOT is intentionally only set when building an image and +# is intentionally empty on a live OpenWrt device -nut_get_notifications() { - local event="$1" - local defaultnotify="$2" +# Shellcheck source paths intentionally point to the location of files of +# the scripts in the development environment (where shellcheck is used), not +# on the live OpenWrt device - if [ "${NUT_EVENT_TYPES#*" $event "}" != "${NUT_EVENT_TYPES}" ]; then - config_get val "$event" message - [ -n "$val" ] && echo "NOTIFYMSG $event \"$val\"" >>"$UPSMON_C" - config_get val "$event" flag "$defaultnotify" - [ -n "$val" ] && echo "NOTIFYFLAG $event $val" >>"$UPSMON_C" - else - logger -t nut-monitor -s "$event is not a valid NUT message event type" - fi +# shellcheck source=net/nut/files/functions.sh.functions +. "${IPKG_INSTROOT}"/lib/functions.sh || { + # Before our sourcing error logging definitions are sourced + logger -s -t nut-monitor "Unable to source 'functions.sh' in 'nut-monitor'. Bailing" + exit 1 } -nut_upsmon_conf() { - local cfg="$1" - local RUNAS val optval defaultnotify - - echo "# Config file automatically generated from UCI config" >"$UPSMON_C" - - config_get RUNAS "$cfg" runas "nutmon" - [ -n "$RUNAS" ] && echo "RUN_AS_USER $RUNAS" >>"$UPSMON_C" - runas="$RUNAS" - - config_get val "$cfg" deadtime 15 - echo "DEADTIME $val" >>"$UPSMON_C" - - config_get val "$cfg" finaldelay 5 - echo "FINALDELAY $val" >>"$UPSMON_C" - - config_get val "$cfg" hostsync 15 - echo "HOSTSYNC $val" >>"$UPSMON_C" - - config_get val "$cfg" minsupplies 1 - echo "MINSUPPLIES $val" >>"$UPSMON_C" - - config_get val "$cfg" nocommwarntime 300 - echo "NOCOMMWARNTIME $val" >>"$UPSMON_C" - - config_get val "$cfg" pollfaillogthrottlemax -1 - echo "POLLFAIL_LOG_THROTTLE_MAX $val" >>"$UPSMON_C" - - config_get val "$cfg" notifycmd - [ -n "$val" ] && echo "NOTIFYCMD \"$val\"" >>"$UPSMON_C" - - config_get val "$cfg" pollfreq 5 - echo "POLLFREQ $val" >>"$UPSMON_C" - - config_get val "$cfg" pollfreqalert 5 - echo "POLLFREQALERT $val" >>"$UPSMON_C" - - echo "POWERDOWNFLAG /var/run/killpower" >>"$UPSMON_C" - - config_get val "$cfg" offduration 30 - echo "OFFDURATION $val" >>"$UPSMON_C" - - config_get val "$cfg" overduration -1 - echo "OVERDURATION $val" >>"$UPSMON_C" - - config_get val "$cfg" oblbduration 0 - echo "OBLBDURATION $val" >>"$UPSMON_C" - - config_get val "$cfg" rbwarntime 43200 - echo "RBWARNTIME $val" >>"$UPSMON_C" - - config_get val "$cfg" alarmcritical 1 - echo "ALARMCRITICAL $val" >>"$UPSMON_C" - - config_get val "$cfg" shutdownexit 0 - echo "SHUTDOWNEXIT $val" >>"$UPSMON_C" - - config_get val "$cfg" shutdowncmd "/usr/sbin/nutshutdown" - echo "SHUTDOWNCMD \"$val\"" >>"$UPSMON_C" - - config_get val "$cfg" certpath - if [ -n "$val" ]; then echo "CERTPATH $val" >>"$UPSMON_C"; fi - - config_get_bool val "$cfg" certverify 0 - if [ -n "$val" ]; then echo "CERTVERIFY $val" >>"$UPSMON_C"; fi - - config_get_bool val "$cfg" forcessl 0 - if [ -n "$val" ]; then echo "FORCESSL $val" >>"$UPSMON_C"; fi - - # debugmin must be a positive integer or zero - config_get val "$cfg" debugmin 0 - case "$val" in - ''|*[!0-9]*) ;; - *) echo "DEBUG_MIN $val" >>"$UPSMON_C" ;; - esac - - config_get val "$cfg" defaultnotify "SYSLOG" - defaultnotify="$val" - config_foreach nut_get_notifications notifications "$val" - - # Otherwise the default is WALL+SYSLOG - for event in $NUT_EVENT_TYPES; do - if ! grep -q "NOTIFYFLAG $event" "$UPSMON_C"; then - echo "NOTIFYFLAG $event $defaultnotify" >>"$UPSMON_C" - fi - done - - havemon=1 +# 'shellcheck' complains about nut-common.sh due to a case pattern it does not understand, even +# though it is correctly POSIX compliant +# shellcheck disable=SC1094 +# shellcheck source=net/nut/files/nut-common.sh.functions +. "${IPKG_INSTROOT}"/lib/functions/nut/nut-common.sh || { + # Before our sourcing error logging definitions are sourced + logger -s -t nut-monitor "Unable to source 'nut-common.sh' in 'nut-monitor'. Bailing" + exit 1 } -nut_upsmon_add() { - local cfg="$1" - local type="$2" - local upsname - local hostname - local port - local powervalue - local username - local password - local system - - config_get upsname "$cfg" upsname - config_get hostname "$cfg" hostname localhost - config_get port "$cfg" port - config_get powervalue "$cfg" powervalue 1 - config_get username "$cfg" username - config_get password "$cfg" password - config_get type "$cfg" type "${type:-secondary}" - - system="$upsname@$hostname" - if [ -n "$port" ]; then - system="$system:$port" - fi - echo "MONITOR $system $powervalue $username $password $type" >>"$UPSMON_C" - - havems=1 +# shellcheck source=net/nut/files/nut-service.sh.functions +. "${IPKG_INSTROOT}"/lib/functions/nut/nut-service.sh || { + log_source_error "nut-service.sh" "nut-monitor" "nut-monitor" + exit 1 } -build_config() { - # shellcheck disable=SC2174 - mkdir -m 0750 -p "$(dirname "$UPSMON_C")" - - config_load nut_monitor - config_foreach nut_upsmon_conf upsmon - config_foreach nut_upsmon_add primary primary - config_foreach nut_upsmon_add secondary secondary - config_foreach nut_upsmon_add monitor - # master and slave are legacy section names. Prefer monitor. - config_foreach nut_upsmon_add master primary - config_foreach nut_upsmon_add slave secondary - - [ ! -s /var/etc/nut/nut.conf ] && { - echo "MODE=netclient" >>/var/etc/nut/nut.conf - chmod 640 /var/etc/nut/nut.conf - chgrp "$(id -gn "${runas:-nutmon}")" /var/etc/nut/nut.conf - } - - [ -s "$UPSMON_C" ] && chmod 640 "$UPSMON_C" - [ -s "$UPSMON_C" ] && chgrp "$(id -gn "${runas:-nutmon}")" "$UPSMON_C" +# /lib/functions/network.sh exists on live OpenWrt systems (base-files), but +# not our development environment. We do not run the script in a development +# environment as it depends on cross-compiled binaries and files being in +# location and having live target names. +# shellcheck disable=SC1091 +# shellcheck source=/dev/null +. "${IPKG_INSTROOT}"/lib/functions/network.sh || { + log_source_error "network.sh" "nut-monitor" "nut-monitor" + exit 1 } -interface_triggers() { - local action="$1" - local triggerlist trigger - - config_get triggerlist "upsmon" triggerlist +# defines NUT_EVENT_TYPES PIDFILE UPSMON_C +# shellcheck source=net/nut/files/nut-monitor-config.sh.functions +. "${IPKG_INSTROOT}"/lib/functions/nut/nut-monitor-config.sh || { + log_source_error "nut-monitor-config.sh" "nut-monitor" "nut-monitor" + exit 1 +} - # shellcheck disable=SC1091 - . "${IPKG_INSTROOT}"/lib/functions/network.sh +restore_umask_on_exit - if [ -n "$triggerlist" ]; then - for trigger in $triggerlist; do - if [ "$action" = "add_trigger" ]; then - procd_add_interface_trigger "interface.*" "$trigger" /etc/init.d/nut-monitor restart - else - network_is_up "$trigger" && return 0 +service_preconditions() { + local action="$1" + # action can be one of start, stop, or reload + + case "$action" in + start | reload) + config_load nut_monitor || { + log_config_load_error "nut_monitor" "nut-monitor" "nut-monitor" + exit 1 + } + + if have_section_of_type "upsmon"; then + # Find the RUNAS user for upsmon + if ! find_runas "upsmon" "nut_monitor" "nutmon" || [ -z "$RUNAS" ]; then + log_error_exit "Failed to determine RUNAS user" "nut-monitor" "nut-monitor" fi - done - else - if [ "$action" = "add_trigger" ]; then - procd_add_raw_trigger "interface.*.up" 2000 /etc/init.d/nut-monitor restart else - ubus call network.device status | grep -q '"up": true' && return 0 + # If we do not have a 'upsmon' section in nut_monitor, use defaults + RUNAS=nutmon fi - fi - [ "$action" = "add_trigger" ] || return 1 -} -start_service() { - local runas=nutmon - local havemon havems - build_config - - [ "$havemon" != 1 ] && return 1 - [ "$havems" != 1 ] && return 1 - interface_triggers "check_interface_up" || return 0 + build_config || return 1 + interface_triggers "check_interface_up" || return 1 + return 0 + ;; + *) + return 0 + ;; + esac +} - procd_open_instance "upsmon" - procd_set_param respawn 10 20 6 - procd_set_param stderr 1 - procd_set_param stdout 0 +start_monitor_instance() { + procd_open_instance upsmon + procd_set_param respawn + procd_set_param stderr 2 + procd_set_param stdout 1 procd_set_param env NUT_QUIET_INIT_UPSNOTIFY=true procd_set_param reload_signal HUP - procd_set_param command /usr/sbin/upsmon -FF + procd_set_param command /usr/sbin/upsmon + procd_append_param command -FF + if [ -n "$RUNAS" ]; then + # upsmon needs root for some actions (like + # shutting down the system, but forks + # and drops root for most of its operation) + procd_append_param command -u "$RUNAS" + fi procd_close_instance +} - return 0 +perform_instance_action() { + local active_upsmon_action="$1" + shift || true + local active_upsmon_signal="$1" + shift || true + local action_command="$1" + shift || true + local action_arg="$1" + shift || true + local active_upsmon_secondary_command="$1" + shift || true + + # If nut-monitor is running but has no instances (even upsmon) + if service_active_no_instances "nut-monitor"; then + log_msg "nut-monitor active with no instances" "nut-monitor" "nut-monitor" "info" + if [ -n "$action_command" ]; then + "$action_command" "$action_arg" + fi + # The only possible instance of nut-monitor in this package is a single + # upsmon instance. Therefore we do not need to check for other instances + elif procd_running nut-monitor upsmon; then + # If nut-monitor has an active upsmon instance + signal_instance "upsmon" \ + "upsmon" \ + "$active_upsmon_action" \ + "$active_upsmon_signal" "$PIDFILE" "" \ + "nut-monitor" \ + "$active_upsmon_secondary_command" \ + "$@" + fi } -# pkill is not available, and pgrep does not accept --signal -pgrepkill() { - local pids +manage_service() { + local action="$1" - [ $# -eq 2 ] || return 1 + # We only support one instance of upsmon, and its UCI + # section must be named 'upsmon' + + service_preconditions "$action" || return 1 + case "$action" in + start) + start_monitor_instance + ;; + reload) + perform_instance_action "reload" "HUP" "start_monitor_instance" + ;; + stop) + # Note that the middle procd_kill is *without* upsmon which stops the procd + # service entirely, not just the instances (so reload triggers will have + # no effect until the service is started again). + perform_instance_action "stop" "TERM" "procd_kill" "nut-monitor" "procd_kill" "nut-monitor" "upsmon" + rm -f "$UPSMON_C" + ;; + stop-instance) + # Stop the upsmon instance but leave nut-monitor procd service active + perform_instance_action "stop" "TERM" "" "" "procd_kill" "nut-monitor" "upsmon" + rm -f "$UPSMON_C" + ;; + *) + # Do not abort on unknown $action, but log it. + log_error "Unknown action: '$action'" "nut-monitor" nut-monitor + ;; + esac +} - pids="$(pgrep "$1" 2>/dev/null)" || return 0 +stop_and_cleanup_on_error() { + manage_service stop-instance + rm -f "$UPSMON_C.new" + rm -f "$UPSMON_C" +} - for pid in $pids; do - kill -"$2" "$pid" - done +start_service() { + manage_service start || stop_and_cleanup_on_error } reload_service() { - local should_stop - - # shellcheck disable=SC1091 - . /lib/functions/procd.sh - - build_config - - should_stop=0 - - [ "$havemon" = 1 ] || should_stop=1 - [ "$havems" = 1 ] || should_stop=1 - interface_triggers "check_interface_up" || should_stop=1 - - if [ "$should_stop" = "0" ]; then - if procd_running nut-monitor upsmon; then - if [ -s "$PIDFILE" ]; then - upsmon -c reload 2>&1 | logger -t nut-monitor - # We don't care about the exit code - return - elif pgrep upsmon >/dev/null 2>/dev/null; then - procd_send_signal nut-monitor upsmon HUP 2>&1 | logger -t nut-monitor - # We don't care about the exit code - return - fi - else - /etc/init.d/nut-monitor start - fi - else - if procd_running nut-monitor upsmon; then - if [ -s "$PIDFILE" ]; then - upsmon -c stop 2>&1 | logger -t nut-monitor - else - pgrepkill upsmon TERM >/dev/null 2>/dev/null - fi - procd_kill nut-monitor upsmon 2>/dev/null | logger -t nut-monitor - fi - fi + manage_service reload || stop_and_cleanup_on_error } stop_service() { - if [ -s "$PIDFILE" ]; then - upsmon -c stop 2>&1 | logger -t nut-monitor - procd_kill nut-monitor 2>/dev/null | logger -t nut-monitor - else - pgrepkill upsmon TERM >/dev/null 2>/dev/null - procd_kill nut-monitor 2>/dev/null | logger -t nut-monitor - fi + manage_service stop } service_triggers() { - config_load nut_monitor - interface_triggers "add_trigger" + interface_triggers "add_trigger" "upsmon" || { + log_error_exit "Failed to add interface triggers" nut-monitor nut-monitor + } procd_add_reload_trigger "nut_monitor" } diff --git a/net/nut/files/nut-notify-exec.default b/net/nut/files/nut-notify-exec.default new file mode 100644 index 0000000000000..242066b1cc82d --- /dev/null +++ b/net/nut/files/nut-notify-exec.default @@ -0,0 +1,78 @@ +#!/bin/sh + +# In recent (relevant) versions of shellcheck busybox is a valid shell type +# shellcheck shell=busybox + +# uci-defaults script to set notify flags to enable execution of notifycmd +# (set separate scripts) unless the user has configured that notifications +# should be ignored. + +# IPKG_INSTROOT is intentionally only set when building an image and +# is intentionally empty on a live OpenWrt device + +# Shellcheck source paths intentionally point to the location of files of +# the scripts in the development environment (where shellcheck is used), not +# on the live OpenWrt device + +# In the nut-upssched package, this script lives in +# /etc/uci-defaults/70-notify-exec +# In the nut-sendmail-notify package, this script lives in +# /etc/uci-defaults/71-notify-exec +# The difference in names is required to avoid conflicting files should both +# packages be installed on the same device. + +# Only run this uci-defaults script on a live OpenWrt device +[ -z "${IPKG_INSTROOT}" ] || exit 0 + +# shellcheck source=net/nut/files/functions.sh.functions +. /lib/functions.sh || { + logger -s -t nut-notify-exec "FATAL: Unable to source 'functions.sh'." || true + exit 1 +} + +# Flags for whether defaultnotify option is set to IGNORE +DEFAULTNOTIFY_IS_IGNORE="false" + +# Load and process nut_monitor (upsmon) configuration +config_load nut_monitor || { + logger -s -t nut-notify-exec "FATAL: Loading 'nut_monitor' failed" || true + exit 1 +} + +config_get val "upsmon" defaultnotify +# shellcheck disable=SC2154 +if [ "${val}" = "IGNORE" ]; then + DEFAULTNOTIFY_IS_IGNORE="true" +else + # Ensure upsmon type section with the name upsmon exists (creating if + # needed, doing nothing if a upsmon type section named 'upsmon' already + # exists). + uci set "nut_monitor.upsmon=upsmon" || { + logger -s -t nut-notify-exec "Failed to ensure an upsmon section named upsmon exists" || true + exit 1 + } +fi + +# Set default notifications to enable notifycmd unless set to ignore +if [ "${DEFAULTNOTIFY_IS_IGNORE}" = "true" ]; then + defaultnotify="IGNORE" +# We only support IGNORE, SYSLOG, and EXEC options to defaultnotify +# Since we want logging (SYSLOG) and require EXEC so that /usr/sbin/upssched +# gets called by NUT, we enforce EXEC as well, producing SYSLOG+EXEC, unless +# the user is ignoring all notifications. +else + defaultnotify="SYSLOG+EXEC" +fi + +uci set "nut_monitor.upsmon.defaultnotify"="$defaultnotify" || { + logger -s -t nut-notify-exec "FATAL: Failed to set 'defaultnotify' for 'upsmon'" || true + uci revert nut_monitor || true + exit 1 +} + +uci commit nut_monitor || { + logger -s -t nut-notify-exec "ERROR: Failed to commit changes to nut_monitor" || true + exit 1 +} + +exit 0 diff --git a/net/nut/files/nut-sched.default b/net/nut/files/nut-sched.default index 4b73b6c61c51e..e0636b4ee937f 100644 --- a/net/nut/files/nut-sched.default +++ b/net/nut/files/nut-sched.default @@ -1,53 +1,69 @@ #!/bin/sh -. "${IPKG_INSTROOT}"/lib/functions.sh - -REMOVEDEFAULTNOTIFY=0 -SKIPADDSYSLOG=0 -SKIPADDEXEC=0 -SKIPADDNOTIFYCMD=0 - -upsmon() { - local cfg="$1" - local val - - config_get val "$cfg" notifycmd - if [ -n "$val" ]; then - SKIPADDNOTIFYCMD=1 - fi - - config_get val "$cfg" defaultnotify - if [ -n "$val" ]; then - if echo "$val" |grep -q "IGNORE"; then - REMOVEDEFAULTNOTIFY=1 - else - SKIPADDSYSLOG=1 - if echo "$val" |grep -q "EXEC"; then - SKIPADDEXEC=1 - fi - fi - fi +# In recent (relevant) versions of shellcheck busybox is a valid shell type +# shellcheck shell=busybox + +# uci-defaults script to setup upsmon notifications using upssched +# applied during install or on first boot after install, if setting on install +# fails. Expects nut-notify-exec to also be run in uci-defaults. + +# IPKG_INSTROOT is intentionally only set when building an image and +# is intentionally empty on a live OpenWrt device + +# Shellcheck source paths intentionally point to the location of files of +# the scripts in the development environment (where shellcheck is used), not +# on the live OpenWrt device. + +# This script lives in the nut-upssched package, which is independent of the +# nut-sendmail-notify package in which nut-sendmail-notify.default lives +# In the nut-upssched package, this script lives in +# /etc/uci-defaults/80-nut-sched + +# The separate packages limit the opportunities for code-sharing across the +# scripts. + +# Only run this uci-defaults script on a live OpenWrt device +[ -z "${IPKG_INSTROOT}" ] || exit 0 + +# shellcheck source=net/nut/files/functions.sh.functions +. /lib/functions.sh || { + logger -s -t nut-sched "FATAL: Unable to source 'functions.sh'" || true + exit 1 } -config_load nut_monitor -config_foreach upsmon upsmon +# Flags for existing notifycmd +SKIP_ADD_NOTIFYCMD="false" -uci set nut_monitor.@upsmon[-1]=upsmon +# Load and process nut_monitor (upsmon) configuration +config_load nut_monitor || { + logger -s -t nut-sched "FATAL: loading 'nut_monitor' failed" || true + exit 1 +} -if [ "$SKIPADDNOTIFYCMD" != "1" ]; then - uci set nut_monitor.@upsmon[-1].notifycmd=/usr/sbin/upssched +config_get val "upsmon" notifycmd +# shellcheck disable=SC2154 +if [ -n "${val}" ]; then + SKIP_ADD_NOTIFYCMD="true" fi -if [ "$REMOVEDEFAULTNOTIFY" = "1" ]; then - uci delete nut_monitor.@upsmon[-1].defaultnotify || true -fi +# If notification command is not set, set it +if [ "${SKIP_ADD_NOTIFYCMD}" = "false" ]; then + # Ensure upsmon type section with the name upsmon exists + uci set "nut_monitor.upsmon=upsmon" || { + logger -s -t nut-sched "Failed to ensure an upsmon section named upsmon exists" || true + exit 1 + } -if [ "$SKIPADDEXEC" != "1" ]; then - uci add_list nut_monitor.@upsmon[-1].defaultnotify="EXEC" -fi + uci set "nut_monitor.upsmon.notifycmd=/usr/sbin/upssched" || { + logger -s -t nut-sched "Failed to set notifycmd to upssched" || true + uci revert nut_monitor || true + exit 1 + } -if [ "$SKIPADDSYSLOG" != "1" ]; then - uci add_list nut_monitor.@upsmon[-1].defaultnotify="SYSLOG" + uci commit nut_monitor || { + logger -s -t nut-sched "Failed to commit changes to nut_monitor" || true + exit 1 + } fi -uci commit nut_monitor +exit 0 diff --git a/net/nut/files/nut-sendmail-notify b/net/nut/files/nut-sendmail-notify old mode 100755 new mode 100644 index f21e41e89accf..f23d51a737262 --- a/net/nut/files/nut-sendmail-notify +++ b/net/nut/files/nut-sendmail-notify @@ -1,12 +1,127 @@ #!/bin/sh -{ -exec /usr/sbin/sendmail root <&2 + } + exit 1 +} + +# 'shellcheck' and 'shfmt' get confused by () and space in the case +# pattern, but () and space are correct for POSIX shells +# shellcheck disable=SC1072,SC1073,SC1085,SC1009 +check_safe_body_string() { + case "$1" in + # Disallow newline (single line body only). Also disallows a single dot on a line (so no + # SMTP dot injection) + *" +"*) + log_send_failure_and_exit "Illegal character in body line for message" + ;; + # We allow as permissive a possible set of characters for the body text + # as possible, without allowing shell injection or requiring more complex + # parsing of the body line. We also avoid % for possible printf + # substitutions + # Some AI code reviewers get confused by \(\) and \space in the case + # pattern, but this is correct for busybox ash in modern OpenWrt + # and does not allow \ through the validation. + *[!a-zA-Z0-9_:!@#^+,*=~.?\(\)/\ -]*) + log_send_failure_and_exit "Illegal character in body line for message" + ;; + esac + return 0 +} + +NUT_NOTIFICATION_DATE="$(date -R)" || { + log_send_failure_and_exit "Failed to get current date and time" +} + +email_content="From: root To: root -Subject: UPS $NOTIFYTYPE Notification +Subject: SUBJECT_PREFIX Notification +Date: MESSAGE_DATE +Content-Type: text/plain; charset=utf-8 -$1 +ONE_BODY_LINE . -EOF -} & +" + +# Set IFS to empty to prevent word splitting +OLD_IFS="$IFS" + +# Restore IFS to allow word-splitting, and reenable globbing, for if this +# script is sourced +# We want this expanded at sourcing time, not execution time +# shellcheck disable=SC2064 +trap "IFS='$OLD_IFS'; set +f" EXIT + +set -f +IFS= +# / is NOTIFYTYPE, NUT_NOTIFICATION_DATE, or $1 are not a security risk, +# because in a POSIX shell variable parameter expansion substitution, the +# content to be substituted (after the second /) inside quotes is taken +# literally. This has been tested on ash with the following simple +# script commands +# +# sub_val="sub/val" +# original="An original with old_val message" +# new="${original/old_val/$sub_val}" +# echo "$new" +# The result output is +# An original with sub/val message +# +# As we are using a single replacement, not global, the presence of a slash (/) +# is not an issue. +email_content="${email_content/SUBJECT_PREFIX/$NOTIFYTYPE}" +email_content="${email_content/MESSAGE_DATE/$NUT_NOTIFICATION_DATE}" +email_content="${email_content/ONE_BODY_LINE/$1}" + +# Mail is only sent if NOTIFYTYPE is set and a body line has been included as a +# parameter ($1), and the {} succeeds (none of the checks exit with +# log_send_failure_and exit) +if [ -n "$NOTIFYTYPE" ] && [ -n "$1" ] && { + command -v sendmail || log_send_failure_and_exit "'sendmail' is not available" + # setsid is available in OpenWrt old stable, stable, and current tip, since before 14a27ac99d + command -v setsid || log_send_failure_and_exit "'setsid' is not available" + # NOTIFYTYPE comes from an UCI section name, so validate for that + check_safe_uci_name "$NOTIFYTYPE" || log_send_failure_and_exit "Illegal name for notification type name." + check_safe_date "$NUT_NOTIFICATION_DATE" || log_send_failure_and_exit "Illegal string for notification date." + check_safe_body_string "$1" || log_send_failure_and_exit "Illegal characters, or more than one line for message body." +}; then + printf '%s' "$email_content" | setsid sendmail root || { + log_send_failure_and_exit "Output to sendmail and initiating sendmail of notification failed." + } +else + exit 1 +fi + +# IFS and globbing will be restored by EXIT trap above +exit 0 diff --git a/net/nut/files/nut-sendmail-notify.default b/net/nut/files/nut-sendmail-notify.default index ebffba55322e0..c8dd1f6827ae9 100644 --- a/net/nut/files/nut-sendmail-notify.default +++ b/net/nut/files/nut-sendmail-notify.default @@ -1,48 +1,73 @@ #!/bin/sh -. "${IPKG_INSTROOT}"/lib/functions.sh - -REMOVEDEFAULTNOTIFY=0 -SKIPADDSYSLOG=0 -SKIPADDEXEC=0 - -upsmon() { - local cfg="$1" - local val - - config_get val "$cfg" defaultnotify - if [ -n "$val" ]; then - if echo "$val" |grep -q "IGNORE"; then - REMOVEDEFAULTNOTIFY=1 - else - SKIPADDSYSLOG=1 - if echo "$val" |grep -q "EXEC"; then - SKIPADDEXEC=1 - fi - fi - fi +# In recent (relevant) versions of shellcheck busybox is a valid shell type +# shellcheck shell=busybox + +# uci-defaults script to configure sendmail notifications for UPS monitor on +# first boot after installation (if not possible during image creation) +# Expects nut-notify-exec to also be run in uci-defaults. + +# IPKG_INSTROOT is intentionally only set when building an image and +# is intentionally empty on a live OpenWrt device + +# Shellcheck source paths intentionally point to the location of files of +# the scripts in the development environment (where shellcheck is used), not +# on the live OpenWrt device + +# This script lives in the nut-sendmail-notify package, which is independent of +# the nut-upssched package in which nut-sched.default lives. +# In the nut-sendmail-notify package, this script lives in +# /etc/uci-defaults/81-sendmail-notify + +# The separate packages limit the opportunities for code-sharing across the +# scripts. + +# Only run this uci-defaults script on a live OpenWrt device +[ -z "${IPKG_INSTROOT}" ] || exit 0 + +# shellcheck source=net/nut/files/functions.sh.functions +. /lib/functions.sh || { + logger -s -t nut-sendmail-notify "FATAL: Unable to source 'functions.sh'" || true + exit 1 } -config_load nut_monitor -config_foreach upsmon upsmon +# Flags for existing notifycmd +SKIP_ADD_NOTIFYCMD="false" -uci set nut_monitor.@upsmon[-1]=upsmon -uci set nut_monitor.@upsmon[-1].notifycmd=/usr/bin/nut-sendmail-notify +# Load and process nut_monitor (upsmon) configuration +config_load nut_monitor || { + logger -s -t nut-sendmail-notify "FATAL: Loading 'nut_monitor' failed." || true + exit 1 +} -if [ "$REMOVEDEFAULTNOTIFY" = "1" ]; then - uci delete nut_monitor.@upsmon[-1].defaultnotify || true +config_get val "upsmon" notifycmd +# shellcheck disable=SC2154 +if [ -n "${val}" ]; then + SKIP_ADD_NOTIFYCMD="true" fi -if [ "$SKIPADDEXEC" != "1" ]; then - if [ "$SKIPADDSYSLOG" != "1" ]; then - uci set nut_monitor.@upsmon[-1].defaultnotify="SYSLOG+EXEC" - else - uci set nut_monitor.@upsmon[-1].defaultnotify="EXEC" - fi -else - if [ "$SKIPADDSYSLOG" != "1" ]; then - uci set nut_monitor.@upsmon[-1].defaultnotify="SYSLOG" - fi +# If notification command is not set, set it +if [ "${SKIP_ADD_NOTIFYCMD}" = "false" ]; then + # Ensure upsmon type section with the name upsmon exists + uci set "nut_monitor.upsmon=upsmon" || { + logger -s -t nut-sendmail-notify "Failed to ensure an upsmon section named upsmon exists" || true + exit 1 + } + + # If nut-upssched (default script nut-sched) is also present, notifycmd will + # already be set and SKIP_ADD_NOTIFYCMD will be true, so this code will + # never be reached. This is intentional. upssched takes precedence over + # nut-sendmail-notify, unless the user writes a script that supports both. + uci set "nut_monitor.upsmon.notifycmd=/usr/bin/nut-sendmail-notify" || { + logger -s -t nut-sendmail-notify "Failed to set notifycmd to be nut-sendmail-notify" || true + uci revert nut_monitor || true + exit 1 + } + + uci commit nut_monitor || { + logger -s -t nut-sendmail-notify "Failed to commit changes to nut_monitor" || true + exit 1 + } fi -uci commit nut_monitor +exit 0 diff --git a/net/nut/files/nut-serial.hotplug b/net/nut/files/nut-serial.hotplug new file mode 100644 index 0000000000000..2492fad6189cc --- /dev/null +++ b/net/nut/files/nut-serial.hotplug @@ -0,0 +1,191 @@ +#!/bin/sh + +# In recent versions of shellcheck, busybox is a valid shell type +# shellcheck shell=busybox + +# OpenWrt hotplug script to set permissions on USB serial +# ports to allow NUT to access UPSes attached via +# USB-to-serial cables + +# OpenWrt hotplug calls this script with DEVNAME and +# ACTION set in the environment + +# IPKG_INSTROOT is intentionally only set when building an image and +# is intentionally empty on a live OpenWrt device + +# shellcheck source=net/nut/files/functions.sh.functions +. "${IPKG_INSTROOT}"/lib/functions.sh || { + # Before our sourcing error logging definitions are sourced + logger -s -t nut-serial-hotplug "Unable to source 'functions.sh' in 'nut-serial' hotplug. Bailing" + exit 1 +} + +# 'shellcheck' complains about nut-common.sh due to a case pattern it does not understand, even +# though it is correctly POSIX compliant +# shellcheck disable=SC1094 +# shellcheck source=net/nut/files/nut-common.sh.functions +. "${IPKG_INSTROOT}"/lib/functions/nut/nut-common.sh || { + # Before our sourcing error logging definitions are sourced + logger -s -t nut-serial-hotplug "Unable to source 'nut-common.sh' in 'nut-serial' hotplug. Bailing" + exit 1 +} + +# shellcheck source=net/nut/files/nut-service.sh.functions +. "${IPKG_INSTROOT}"/lib/functions/nut/nut-service.sh || { + log_source_error "nut-service.sh" "nut-serial" "nut-serial-hotplug" + exit 1 +} + +# shellcheck source=net/nut/files/nut-server-service.sh.functions +. "${IPKG_INSTROOT}"/lib/functions/nut/nut-server-service.sh || { + log_source_error "nut-server-service.sh" "nut-serial" "nut-serial-hotplug" + exit 1 +} + +# 'shellcheck' does not understand config_foreach and therefore treats +# functions and variables referenced only from config_foreach as not reachable, +# or never invoked. +# shellcheck disable=SC2317,SC2329 +nut_set_serial_port_permissions() { + local devname="$1" + local ups="$2" + local device_group + + # Get group of RUNAS user, to set the group of the hotplugged device node + # If RUNAS user does not exist, exit with an error. + device_group="$(id -gn "${RUNAS:-nut}")" || { + log_error "Unable to find group for '$RUNAS'. Skipping ups '$ups'" nut-serial nut-serial-hotplug + return 1 + } + + # Guard against disappearing device (e.g. due to 'bouncing' device) + if [ -e "/dev/$devname" ]; then + if ! chgrp "$device_group" "/dev/$devname"; then + log_error "Unable to set group on '$devname'" nut-serial nut-serial-hotplug + return 1 + fi + else + log_error "'$devname' disappeared before setting group" nut-serial nut-serial-hotplug + return 1 + fi + + # Device could disappear between chgrp and chmod + if [ -e "/dev/$devname" ]; then + if ! chmod g+rw "/dev/$devname"; then + log_error "Unable to set permissions on '$devname'" nut-serial nut-serial-hotplug + return 1 + fi + else + log_error "'$devname' disappeared before setting permissions" nut-serial nut-serial-hotplug + return 1 + fi + return 0 +} + +# If enable_usb_serial is set, set the permissions on applicable USB +# serial devices to allow NUT access +# We do not exit with error as that would stop processing of other UPS config +# sections. +# We use 'did_set_perms' as a 'pseudo-global' from nut_on_hotplug_add in order +# to return a value to config_foreach + +# 'shellcheck' does not understand config_foreach and therefore treats +# functions and variables referenced only from config_foreach as not reachable, +# or never invoked. +# shellcheck disable=SC2317,SC2329 +nut_serial() { + local ups="$1" # UCI section name is also the UPS configuration name + local enable_usb_serial port + + config_get_bool enable_usb_serial "$ups" enable_usb_serial 0 + config_get port "$ups" port + + [ "$enable_usb_serial" = "1" ] && { + # If port is specified only change tty's matching port + if [ -n "$port" ] && [ "${port#/dev/}" = "${port}" ]; then + port="/dev/$port" + fi + if [ -n "$DEVNAME" ] && [ "${DEVNAME#/dev/}" = "${DEVNAME}" ]; then + DEVNAME="/dev/$DEVNAME" + fi + # Empty port from configuration means match any device. + if [ "$port" = "$DEVNAME" ] || [ "$port" = "" ]; then + if nut_set_serial_port_permissions "$DEVNAME" "$ups"; then + did_set_perms="true" + fi + fi + } +} + +nut_on_hotplug_add() { + # We use did_set_perms as a 'pseudo-global'. It is visible and can be + # modified by called functions, but the changes do not propagate up from + # this function. + local did_set_perms="false" + + config_load nut_server || { + log_config_load_error "nut_server" "nut-serial" "nut-serial-hotplug" + exit 1 + } + + # Only find statepath and runas once per event + # sets RUNAS + find_runas "upsd" "nut_server" || { + log_error_exit "Failed to set RUNAS" nut-serial nut-serial-hotplug + } + + # Check if serial USB UPS for each NUT driver instance (UPS), and if so + # configure permissions to allow NUT to access the port + config_foreach nut_serial driver + + # If we set permissions on a device that may have a UPS attached, run + # the UPS (NUT) server reload to check for UPS configuration and + # attempt to reload the driver and/or server + if [ "$did_set_perms" = "true" ]; then + # Informational and not an error + logger -t nut-serial-hotplug "Successfully set permissions for serial device(s)" + # We shouldn't get here if hotplug is disabled (race condition if we do), so log it as an error + if ! allow_hotplug_restart; then + log_error "Did not restart NUT after setting permissions as hotplug restart is disabled" nut-serial nut-serial-hotplug + # Disabled hotplug is an allowed state + return 0 + fi + if ! /etc/init.d/nut-server enabled; then + # shellcheck disable=SC2153 + log_error "NUT server disabled when processing hotplug event 'add' for '$DEVNAME'." nut-serial nut-serial-hotplug + # Disabled nut-server is an allowed state + return 0 + else + # Initscript should have its own logging + /etc/init.d/nut-server reload || return 1 + fi + fi + return 0 +} + +# Do not do any hotplug actions if hotplug is disabled. Not an error but log +# for information +if ! allow_hotplug_restart; then + # Informational and not an error + logger -t nut-serial-hotplug "Did not set permissions on serial port hotplug as NUT hotplug is disabled" + exit 0 +fi + +if ! [ "$(id -u)" = "0" ]; then + log_error_exit "Cannot set update device node on serial port hotplug. Hotplug not running as root." nut-serial nut-serial-hotplug +fi + +# Only act on hotplug events with ACTION 'add' and where +# the hotplugged device is a USB serial port +# shellcheck disable=SC2153 +if [ "$ACTION" = "add" ] && [ -n "$DEVNAME" ]; then + # On add of a USB serial port with name + # ttyUSB* or ttyAMA* or ttyACM* or ttyGS* + case "$DEVNAME" in + ttyUSB* | ttyAMA* | ttyACM* | ttyGS*) + nut_on_hotplug_add || exit 1 + ;; + esac +fi + +exit 0 diff --git a/net/nut/files/nut-server-config.sh.functions b/net/nut/files/nut-server-config.sh.functions new file mode 100644 index 0000000000000..e1f1b5521e6f7 --- /dev/null +++ b/net/nut/files/nut-server-config.sh.functions @@ -0,0 +1,386 @@ +#!/bin/sh +# Shebang line included so editors and shellcheck/shfmt know this contains +# shell code + +# Create configuration for upsd and UPS drivers for NUT + +# In recent (relevant) versions of shellcheck busybox is a valid shell type +# shellcheck shell=busybox + +# Pre-requisite sourcing for functions this script uses +# * /lib/functions.sh has been sourced +# * /lib/functions/nut/nut-common.sh has been sourced +# * /lib/functions/nut/nut-service.sh has been sourced + +# Ensure find_statepath from nut-service.sh is executed +# before srv_config in this file. + +# Ephemeral files, recreated on each OpenWrt boot +USERS_C=/var/etc/nut/upsd.users # NUT user and password file +UPSD_C=/var/etc/nut/upsd.conf # NUT upsd (UPS daemon) configuration +UPS_C=/var/etc/nut/ups.conf # NUT UPS configuration (ini-style section per UPS) +NUT_CONF=/var/etc/nut/nut.conf # NUT mode config + +# config_load is done by the sourcing script, before executing any functions in +# this file + +# Writes configuration values to the UPS run-time configuration file +get_write_ups_config() { + local ups="$1" + local var="$2" + local def="$3" + local flag="$4" + local config_file="$5" + local val + + if [ -z "$flag" ] || [ "$flag" = "0" ]; then + config_get val "$ups" "$var" "$def" + [ -n "$val" ] && printf "%s = %s\n" "$var" "$val" >>"$config_file" + else + config_get_bool val "$ups" "$var" "$def" + # In NUT config a flag is either present or not present + # present is true, not present is false. Map UCI bool to NUT flags. + if [ "$val" = "1" ]; then + printf "%s\n" "$var" >>"$config_file" + fi + fi +} + +# Add upsd listen address and port to the run-time configuration +listen_address() { + local srv="$1" + local config_file="$2" + local address port + + config_get address "$srv" address "::1" + config_get port "$srv" port + if [ -n "$port" ]; then + printf "LISTEN %s %s\n" "$address" "$port" >>"$config_file" + else + printf "LISTEN %s\n" "$address" >>"$config_file" + fi +} + +# Adds upsd (NUT server) run-time configuration +srv_config() { + local srv="$1" + local config_file="$2" + local maxage maxconn certfile + + config_get maxage "$srv" maxage + [ -n "$maxage" ] && printf "MAXAGE %s\n" "$maxage" >>"$config_file" + + [ -n "$STATEPATH" ] && printf "STATEPATH %s\n" "$STATEPATH" >>"$config_file" + + config_get maxconn "$srv" maxconn + [ -n "$maxconn" ] && printf "MAXCONN %s\n" "$maxconn" >>"$config_file" + + #NOTE: certs only apply to SSL-enabled version + config_get certfile "$srv" certfile + [ -n "$certfile" ] && printf "CERTFILE %s\n" "$certfile" >>"$config_file" +} + +nut_user_instcmd() { + local val="$1" + local config_file="$2" + printf " instcmds = %s\n" "$val" >>"$config_file" +} + +# Adds run-time configuration for NUT users to the upsd.users file +nut_user_add() { + local user="$1" + local config_file="$2" + local a + local val + + config_get val "$user" username "$user" + printf "[%s]\n" "$val" >>"$config_file" + + config_get val "$user" password + [ -n "$val" ] && printf " password = %s\n" "$val" >>"$config_file" + + config_get val "$user" actions + set -f + for a in $val; do + printf " actions = %s\n" "$a" >>"$config_file" + done + set +f + + # The name instcmd is use for both the UCI option and the callback function + config_list_foreach "$user" instcmd nut_user_instcmd "$config_file" + + config_get val "$user" upsmon + if [ -n "$val" ]; then + printf " upsmon %s\n" "$val" >>"$config_file" + fi +} + +# Builds upsd (NUT server) run-time configuration +build_server_config() { + local conf_group="$1" + local nut_conf_err="false" + + umask 022 + mkdir -p "$(dirname "$UPSD_C")" || { + log_error "Failed to create directory for upsd configuration file '$UPSD_C'" nut-server-config.sh nut-server-config + return 1 + } + + rm -f "$USERS_C.new" + rm -f "$UPSD_C.new" + + umask 117 + echo "# Config file automatically generated from UCI config" >"$USERS_C.new" + echo "# Config file automatically generated from UCI config" >"$UPSD_C.new" + umask 133 + touch "$NUT_CONF" + restore_umask + + # For nut_user and listen_address, the section type and callback function + # are named the same + config_foreach nut_user_add user "$USERS_C.new" + config_foreach listen_address listen_address "$UPSD_C.new" + if have_section_of_type "upsd"; then + config_foreach srv_config upsd "$UPSD_C.new" + else + # If config 'nut_server' does not have a 'upsd' section, use a default + # configuration + [ -n "$STATEPATH" ] && printf "STATEPATH %s\n" "$STATEPATH" >>"$UPSD_C.new" + fi + + # Failure to write nut.conf is not hard-fatal although it means the NUT will + # not start the service. + # Also, we only write nut.conf if there is not one already + if [ ! -s "$NUT_CONF" ]; then + umask 133 + if ! printf "MODE=netserver\n" >"$NUT_CONF"; then + log_error "nut-server creation of nut.conf failed" nut-server-config.sh nut-server-config + return 1 + fi + else + # Otherwise if nut-server is already configured, make sure both + # nut-server (this service) and nut-monitor are started + if grep -q '^MODE=netclient' "$NUT_CONF"; then + # In modern OpenWrt 'sed -i' modifies the specified files, without backup + sed -i -e 's/^MODE=netclient/MODE=both/' "$NUT_CONF" || { + log_error "Failed to update nut.conf to support both upsmon and upsd" nut-server-config.sh nut-server-config + nut_conf_err="true" + } + fi + fi + + if [ "$nut_conf_err" = "false" ] && [ -n "$conf_group" ]; then + chgrp "$conf_group" "$USERS_C.new" || { + log_error "failed to set group on '$USERS_C.new'" nut-server-config.sh nut-server-config + nut_conf_err="true" + } + chgrp "$conf_group" "$UPSD_C.new" || { + log_error "failed to set group on '$UPSD_C.new'" nut-server-config.sh nut-server-config + nut_conf_err="true" + } + fi + + # shellcheck disable=SC2034 + [ "$nut_conf_err" = "false" ] || return 1 +} + +# shellcheck disable=SC2317,SC2329 +nut_ups_defoverride() { + local overvar="$1" + local defover="$2" + local config_file="$3" + local ups="$4" + local overtype + local overval + + # tr is used as global replace in variable parameter expansion is not + # available in ash. + overtype="$(printf "%s" "$overvar" | tr '_' '.')" + + config_get overval "$ups" "${defover}_${overvar}" + [ -n "$overval" ] && printf "%s.%s = %s\n" "${defover}" "${overtype}" "$overval" >>"$config_file" +} + +# From documentation and fixing of nut_ups_other (previously other or do_other) +# function (below) in https://github.com/rpavlik in +# https://github.com/openwrt/packages/pull/28308 +# +# For each value "x" in the per-driver ($ups) list "other", get the value of +# per-driver config variable "other_x", and if it is not empty, +# write "x = {value of other_x}" to the config file. +# For each value "x" in the per-driver ($ups) list "otherflag", get the value of +# per-driver config variable "otherflag_x", and if it is true, write +# "x" to the config file. + +# shellcheck disable=SC2317,SC2329 +nut_ups_other() { + local othervar="$1" + local othervarflag="$2" + local config_file="$3" + local ups="$4" + local otherval + + if [ "$othervarflag" = "otherflag" ]; then + config_get_bool otherval "${ups}" "${othervarflag}_${othervar}" + [ "$otherval" = "1" ] && printf "%s\n" "${othervar}" >>"$config_file" + else + config_get otherval "${ups}" "${othervarflag}_${othervar}" + [ -n "$otherval" ] && printf "%s = %s\n" "${othervar}" "$otherval" >>"$config_file" + fi +} + +# Builds UPS-specific run-time configuration +build_ups_config() { + local ups="$1" + local config_file="$2" + + printf "[%s]\n" "$ups" >>"$config_file" + + get_write_ups_config "$ups" bus "" "" "$config_file" + get_write_ups_config "$ups" cable "" "" "$config_file" + get_write_ups_config "$ups" community "" "" "$config_file" + get_write_ups_config "$ups" desc "" "" "$config_file" + get_write_ups_config "$ups" driver "usbhid-ups" "" "$config_file" + get_write_ups_config "$ups" ignorelb 0 1 "$config_file" + get_write_ups_config "$ups" interruptonly 0 1 "$config_file" + get_write_ups_config "$ups" interruptsize "" "" "$config_file" + get_write_ups_config "$ups" maxreport "" "" "$config_file" + get_write_ups_config "$ups" maxstartdelay "" "" "$config_file" + get_write_ups_config "$ups" mfr "" "" "$config_file" + get_write_ups_config "$ups" model "" "" "$config_file" + get_write_ups_config "$ups" nolock 0 1 "$config_file" + get_write_ups_config "$ups" notransferoids 0 1 "$config_file" + get_write_ups_config "$ups" offdelay "" "" "$config_file" + get_write_ups_config "$ups" ondelay "" "" "$config_file" + get_write_ups_config "$ups" pollfreq "" "" "$config_file" + get_write_ups_config "$ups" port "auto" "" "$config_file" + get_write_ups_config "$ups" product "" "" "$config_file" + get_write_ups_config "$ups" productid "" "" "$config_file" + get_write_ups_config "$ups" retrydelay "" "" "$config_file" + get_write_ups_config "$ups" sdorder "" "" "$config_file" + get_write_ups_config "$ups" sdtime "" "" "$config_file" + get_write_ups_config "$ups" serial "" "" "$config_file" + get_write_ups_config "$ups" shutdown_delay "" "" "$config_file" + get_write_ups_config "$ups" snmp_version "" "" "$config_file" + get_write_ups_config "$ups" snmp_retries "" "" "$config_file" + get_write_ups_config "$ups" snmp_timeout "" "" "$config_file" + get_write_ups_config "$ups" synchronous "" "" "$config_file" + get_write_ups_config "$ups" usd "" "" "$config_file" + get_write_ups_config "$ups" vendor "" "" "$config_file" + get_write_ups_config "$ups" vendorid "" "" "$config_file" + + # Params specific to NetXML driver + get_write_ups_config "$ups" login "" "" "$config_file" + get_write_ups_config "$ups" password "" "" "$config_file" + get_write_ups_config "$ups" subscribe 0 1 "$config_file" + + config_list_foreach "$ups" override nut_ups_defoverride override "$config_file" "$ups" + config_list_foreach "$ups" default nut_ups_defoverride default "$config_file" "$ups" + + # For each entry in the "$ups" list "other", get the + # variable "other_${entry}", and if not empty, write + # "${entry} = ${value of other_${entry}}" to the config file. + config_list_foreach "$ups" other nut_ups_other other "$config_file" "$ups" + + # For each entry in the "$ups" list "otherflag", get the + # variable "otherflag_${entry}", and if true, write "${entry}" to + # the config file. + config_list_foreach "$ups" otherflag nut_ups_other otherflag "$config_file" "$ups" + + echo "" >>"$config_file" + # shellcheck disable=SC2034 + haveupscfg=true +} + +# Add global drivers settings to run-time config +build_global_driver_config() { + local cfg="$1" + local config_file="$2" + + # Global driver config + get_write_ups_config "$cfg" chroot "" "" "$config_file" + get_write_ups_config "$cfg" driverpath "" "" "$config_file" + get_write_ups_config "$cfg" maxstartdelay "" "" "$config_file" + get_write_ups_config "$cfg" maxretry "" "" "$config_file" + get_write_ups_config "$cfg" retrydelay "" "" "$config_file" + get_write_ups_config "$cfg" pollinterval "" "" "$config_file" + get_write_ups_config "$cfg" synchronous "" "" "$config_file" + echo "" >>"$config_file" +} + +# Orchestrate the run-time configuration building process +build_config() { + local conf_group + # 'pseudo-global' to allow config_foreach build_ups_config to signal that + # at least one UPS section was configured. We do not want return 1 in + # the config_foreach as that would abort processing for other UPS sections. + local haveupscfg="false" + + if [ -z "$RUNAS" ]; then + log_error "RUNAS is empty; cannot determine group for configuration files" nut-server-config.sh nut-server-config + return 1 + fi + + if ! conf_group="$(id -gn "$RUNAS")"; then + log_error "Failed to determine group for user '$RUNAS'" nut-server-config.sh nut-server-config + return 1 + fi + [ -n "$conf_group" ] || return 1 + + # shellcheck disable=SC2153 + [ -d "${STATEPATH}" ] || { + umask 007 + mkdir -p "${STATEPATH}" || { + # We hard fail here, as we cannot rely on service state if + # STATEPATH does not exist + log_error "Failed to create ${STATEPATH}." nut-server nut-server + # return so we can do cleanup + return 1 + } + if [ -n "$conf_group" ]; then + chown "root:${conf_group}" "${STATEPATH}" || { + log_error "Failed to change owner:group on STATEPATH" nut-server-config.sh nut-server-config + return 1 + } + fi + } + + # At this point we know STATEPATH exists and we have either created it with + # the required owner, group, and permissions, or it was created elsewhere. + # We do not second-guess other sources of creating this STATEPATH and + # presume that as long as we have read and traversal access, that the + # permissions are intentional. As 'stat' is not available by default in + # OpenWrt we just check if we can read and traverse the directory. + if [ ! -r "$STATEPATH" ] || [ ! -x "$STATEPATH" ]; then + log_error "We do not have access to STATEPATH '$STATEPATH'." nut-server-config.sh nut-server-config + return 1 + fi + + # This directory is shared with the nut-monitor which runs as a different + # user and group, so must be all readable. We set the ownership and + # permissions on individual files more restrictively, as needed + # shellcheck disable=SC2174 + umask 022 + mkdir -p "$(dirname "$UPS_C")" + rm -f "$UPS_C" + umask 137 + echo "# Config file automatically generated from UCI config" >"$UPS_C.new" + chgrp "$conf_group" "$UPS_C.new" || { + log_error "Failed to change group on UPS_C.new (new UPS config)" nut-server-config.sh nut-server-config + return 1 + } + + config_foreach build_global_driver_config driver_global "$UPS_C.new" + config_foreach build_ups_config driver "$UPS_C.new" + + [ "$haveupscfg" = "true" ] || { + log_msg "No configured UPS. Not starting upsd." nut-server-config.sh nut-server-config warn + return 1 + } + + if ! build_server_config "$conf_group"; then + log_msg "Failed to configure upsd. Not starting." nut-server-config.sh nut-server-config warn + return 1 + fi + return 0 +} diff --git a/net/nut/files/nut-server-service.sh.functions b/net/nut/files/nut-server-service.sh.functions new file mode 100644 index 0000000000000..7e7523d2c162a --- /dev/null +++ b/net/nut/files/nut-server-service.sh.functions @@ -0,0 +1,202 @@ +#!/bin/sh +# Shebang line included so editors and shellcheck/shfmt know this contains +# shell code + +# Helper functions for NUT server service handling + +# In recent (relevant) versions of shellcheck busybox is a valid shell type +# shellcheck shell=busybox + +# config_load is done by the sourcing script, before executing any functions in +# this file + +# Pre-requisite sourcing for functions this script uses +# * /lib/functions.sh has been sourced +# * /lib/functions/nut/nut-common.sh has been sourced + +# find_runas to find the RUNAS user should already have been +# run by nut-server initscript prior to calling +# ensure_usb_ups_access. + +# Ensure the RUNAS user has access to the USB UPS device +ensure_usb_ups_access() { + local ups="$1" + local known_devname="$2" + local cfg_vendorid + local cfg_productid + local device_owner + local device_group + local usb_bus + local usb_dev + local device_path + local cur_devname + local device_serial + local sysfs_vendorid + local sysfs_productid + local serial + # When no USB bus or USB device is found, the value returned in each case, + # after printf, will be '000', which is not a valid USB bus or USB device + # number (i.e. it is a missing data indicator). + local missing_usb_num="000" + + device_owner="$RUNAS" + device_group="$(id -gn "$RUNAS")" || { + log_error "Unable to find group for RUNAS='$RUNAS'. Skipping ups '$ups'" nut-server-service.sh nut-server-service + return 1 + } + + config_get cfg_vendorid "$ups" vendorid + # replacement in variable parameter expansion is supported by ash in OpenWrt + # since at least OpenWrt-24.10 (the previous stable release, which two + # versions older than the targetted next release of OpenWrt (after + # OpenWrt-25.12). This has been verified by the author of this script). + if [ -n "$cfg_vendorid" ] && ! check_valid_hex_number "${cfg_vendorid/0x/}"; then + log_error "Configured vendorid for '$ups' is not valid" nut-server-service.sh nut-server-service + return 1 + fi + + # Configuration might not have a vendorid. This is allowed, but we have + # nothing to do here in that case. + [ -n "$cfg_vendorid" ] || return 0 + + case "$cfg_vendorid" in + 0x*) + cfg_vendorid="$(printf "%04x" "$cfg_vendorid")" + ;; + *) + cfg_vendorid="$(printf "%04x" "0x$cfg_vendorid")" + ;; + esac + + config_get cfg_productid "$ups" productid + if [ -n "$cfg_productid" ] && ! check_valid_hex_number "${cfg_productid/0x/}"; then + log_error "Configured productid for '$ups' is not valid" nut-server-service.sh nut-server-service + return 1 + fi + + # Configuration might not have a productid. This is allowed, but we have + # nothing to do here in that case. + [ -n "$cfg_productid" ] || return 0 + + case "$cfg_productid" in + 0x*) + cfg_productid="$(printf "%04x" "$cfg_productid")" + ;; + *) + cfg_productid="$(printf "%04x" "0x$cfg_productid")" + ;; + esac + + config_get serial "$ups" serial + + # $'\x' syntax is supported by ash in OpenWrt since at least OpenWrt-24.10 + # (the previous stable release, which two versions older than the targetted + # next release of OpenWrt (after OpenWrt-25.12). This has been verified by + # the author of this script). + # shellcheck disable=SC3003 + local NL=$'\n' + + # Loop on the idVendor of all USB devices attached to the system + # We do not need any return values from this loop, which exists solely for the side-effects of + # setting the appropriate device permissions and group, so subshell is okay. + # IFS is set to newline *only*. Default IFS also includes tab and space + # Disable globbing. `find` will still use patterns. + set -f + find /sys/devices -name idVendor -a -path '*/usb*/*' | while IFS="$NL" read -r vendor_path; do + set +f + usb_bus="" + usb_dev="" + device_path="" + cur_devname="" + device_serial="" + # Filter by vendor ID first + + sysfs_vendorid="$(cat "$vendor_path")" + if [ -z "$sysfs_vendorid" ]; then + log_error "Could not read vendor ID from $vendor_path" nut-server-service.sh nut-server-service + continue + fi + + if [ "$sysfs_vendorid" != "$cfg_vendorid" ]; then + continue + fi + + device_path="$(dirname "$vendor_path")" + + # Then filter by product ID + + sysfs_productid="$(cat "$device_path/idProduct")" + + if [ -z "$sysfs_productid" ]; then + log_error "Could not read product ID from $device_path" nut-server-service.sh nut-server-service + continue + fi + if [ "$sysfs_productid" != "$cfg_productid" ]; then + continue + fi + + # Next filter by serial (number), if provided + + # guard against disappearing device_path (e.g. due to 'bouncing' device connection) + if [ ! -e "$device_path" ]; then + continue + fi + + # Only check against device serial number if a serial number is + # in the configuration for this UPS + if [ -n "$serial" ]; then + device_serial="$(cat "$device_path/serial" 2>/dev/null)" + + if [ "$serial" != "$device_serial" ]; then + continue + fi + 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"' + if [ "$usb_bus" = "$missing_usb_num" ] || [ "$usb_dev" = "$missing_usb_num" ]; then + # If this sysfs device is not valid, keep looking (and ignore this + # sysfs device). + continue + else + # If we are called from hotplug, we know the device name, so skip any other devices + cur_devname="/dev/bus/usb/$usb_bus/$usb_dev" + if [ -n "$known_devname" ] && [ "$known_devname" != "$cur_devname" ]; then + continue + fi + + # guard against disappearing cur_devname (e.g. due to 'bouncing' device connection) + if [ -e "$cur_devname" ]; then + chmod 0660 "$cur_devname" || { + log_error "'chmod' failed for $cur_devname (ups '$ups')" nut-server-service.sh nut-server-service + continue + } + # Re-check in case device disappeared after chmod + if [ -e "$cur_devname" ]; then + chown "${device_owner}:${device_group}" "$cur_devname" || { + log_error "'chown' failed for $cur_devname (ups '$ups')" nut-server-service.sh nut-server-service + continue + } + fi + else + continue + fi + + # Serial numbers are defined as unique, so do not loop further (on /sys/devices - while), + # if serial was present and matched + if [ -n "$serial" ]; then + # Break out of while loop (so we do not process more 'find' results) + break + fi + # If a serial number is not provided we need all vendor:product matches + # to have permissions for NUT as we do not know the method used + # to match UPS device to a configuration, in this script + # (but if we have a known_devname, limit to that device) + fi + set -f + done + set +f +} diff --git a/net/nut/files/nut-server.init b/net/nut/files/nut-server.init old mode 100755 new mode 100644 index 6365f60eb6b72..b9ed2abfae96e --- a/net/nut/files/nut-server.init +++ b/net/nut/files/nut-server.init @@ -1,674 +1,491 @@ #!/bin/sh /etc/rc.common # Copyright © 2012-2026 OpenWrt.org # +# Startup script for NUT (UPS) server +# # This is free software, licensed under the GNU General Public License v2. # See /LICENSE for more information. # -# shellcheck shell=ash +# In recent (relevant) versions of shellcheck busybox is a valid shell type +# shellcheck shell=busybox # shellcheck disable=SC2034 START=70 STOP=30 -USERS_C=/var/etc/nut/upsd.users -UPSD_C=/var/etc/nut/upsd.conf -UPS_C=/var/etc/nut/ups.conf - USE_PROCD=1 -get_write_ups_config() { - local ups="$1" - local var="$2" - local def="$3" - local flag="$4" - local val - - [ -z "$flag" ] && { - config_get val "$ups" "$var" "$def" - [ -n "$val" ] && [ "$val" != "0" ] && echo "$var = $val" >>"$UPS_C" - } +# IPKG_INSTROOT is intentionally only set when building an image and +# is intentionally empty on a live OpenWrt device - [ -n "$flag" ] && { - config_get_bool val "$ups" "$var" "$def" - [ "$val" = 1 ] && echo "$var" >>"$UPS_C" - } -} +# The shellcheck source directives are used during development by external +# tools to find the source files on the development system. They do not +# affect runtime behaviour. -srv_statepath() { - local statepath +# Also the filenames in the shellcheck source directives may differ from the +# production names that are sourced on live OpenWrt devices. - config_get statepath upsd statepath /var/run/nut - STATEPATH="$statepath" +# shellcheck source=net/nut/files/functions.sh.functions +. "${IPKG_INSTROOT}"/lib/functions.sh || { + # Before our sourcing error logging definitions are sourced + logger -s -t "nut-server" "FATAL: Unable to source 'functions.sh' in 'nut-server'" + exit 1 } -srv_runas() { - local runas - - [ -n "$RUNAS" ] && return 0 - - config_get runas upsd runas nut - RUNAS="$runas" +# 'shellcheck' complains about nut-common.sh due to a case pattern it does not understand, even +# though it is correctly POSIX compliant +# shellcheck disable=SC1094 +# shellcheck source=net/nut/files/nut-common.sh.functions +. "${IPKG_INSTROOT}"/lib/functions/nut/nut-common.sh || { + # Before our sourcing error logging definitions are sourced + logger -s -t "nut-server" "FATAL: Unable to source 'nut-common.sh' in 'nut-server'" + exit 1 } -listen_address() { - local srv="$1" - - config_get address "$srv" address "::1" - config_get port "$srv" port - # shellcheck disable=SC2154 - echo "LISTEN $address $port" >>"$UPSD_C" +# shellcheck source=/dev/null +. "${IPKG_INSTROOT}"/lib/functions/network.sh || { + log_source_error "network.sh" "nut-server" "nut-server" + exit 1 } -srv_config() { - local srv="$1" - local maxage maxconn certfile runas statepath - - # Note runas support requires you make sure USB device file is readable by - # the runas user - config_get runas "$srv" runas nut - RUNAS="$runas" - - config_get statepath "$srv" statepath /var/run/nut - STATEPATH="$statepath" - - config_get maxage "$srv" maxage - [ -n "$maxage" ] && echo "MAXAGE $maxage" >>"$UPSD_C" - - [ -n "$statepath" ] && echo "STATEPATH $statepath" >>"$UPSD_C" - - config_get maxconn "$srv" maxconn - [ -n "$maxconn" ] && echo "MAXCONN $maxconn" >>"$UPSD_C" - - #NOTE: certs only apply to SSL-enabled version - config_get certfile "$srv" certfile - [ -n "$certfile" ] && echo "CERTFILE $certfile" >>"$UPSD_C" +# shellcheck source=net/nut/files/nut-server-config.sh.functions +. "${IPKG_INSTROOT}"/lib/functions/nut/nut-server-config.sh || { + log_source_error "nut-server-config.sh" "nut-server" "nut-server" + exit 1 } -nut_user_add() { - local user="$1" - local a - local val - - config_get val "$user" username "$1" - echo "[$val]" >> "$USERS_C" - - config_get val "$user" password - echo " password = $val" >> "$USERS_C" - - config_get val "$user" actions - for a in $val; do - echo " actions = $a" >> "$USERS_C" - done - - instcmd() { - # shellcheck disable=2317 - local val="$1" - # shellcheck disable=2317 - echo " instcmds = $val" >> "$USERS_C" - } - - config_list_foreach "$user" instcmd instcmd - - config_get val "$user" upsmon - if [ -n "$val" ]; then - echo " upsmon $val" >> "$USERS_C" - fi +# shellcheck source=net/nut/files/nut-service.sh.functions +. "${IPKG_INSTROOT}"/lib/functions/nut/nut-service.sh || { + log_source_error "nut-service.sh" "nut-server" "nut-server" + exit 1 } -build_server_config() { - mkdir -p "$(dirname "$UPSD_C")" - chmod 0640 "$UPS_C" - rm -f "$USERS_C" - rm -f "$UPSD_C" - rm -f /var/etc/nut/nut.conf - - echo "# Config file automatically generated from UCI config" > "$USERS_C" - echo "# Config file automatically generated from UCI config" > "$UPSD_C" - - config_foreach nut_user_add user - config_foreach listen_address listen_address - config_foreach srv_config upsd - echo "MODE=netserver" >>/var/etc/nut/nut.conf - - chmod 0640 "$USERS_C" - chmod 0640 "$UPSD_C" - chmod 0644 /var/etc/nut/nut.conf - - if [ -n "$RUNAS" ]; then - chgrp "$(id -gn "$RUNAS")" "$USERS_C" - chgrp "$(id -gn "$RUNAS")" "$UPSD_C" - fi - havesrvcfg=1 +# shellcheck source=net/nut/files/nut-server-service.sh.functions +. "${IPKG_INSTROOT}"/lib/functions/nut/nut-server-service.sh || { + log_source_error "nut-server-service.sh" "nut-server" "nut-server" + exit 1 } -build_ups_config() { - local ups="$1" - - echo "[$ups]" >>"$UPS_C" - - get_write_ups_config "$ups" bus - get_write_ups_config "$ups" cable - get_write_ups_config "$ups" community - get_write_ups_config "$ups" desc - get_write_ups_config "$ups" driver "usbhid-ups" - get_write_ups_config "$ups" ignorelb 0 1 - get_write_ups_config "$ups" interruptonly 0 1 - get_write_ups_config "$ups" interruptsize - get_write_ups_config "$ups" maxreport - get_write_ups_config "$ups" maxstartdelay - get_write_ups_config "$ups" mfr - get_write_ups_config "$ups" model - get_write_ups_config "$ups" nolock 0 1 - get_write_ups_config "$ups" notransferoids 0 1 - get_write_ups_config "$ups" offdelay - get_write_ups_config "$ups" ondelay - get_write_ups_config "$ups" pollfreq - get_write_ups_config "$ups" port "auto" - get_write_ups_config "$ups" product - get_write_ups_config "$ups" productid - get_write_ups_config "$ups" retrydelay - get_write_ups_config "$ups" sdorder - get_write_ups_config "$ups" sdtime - get_write_ups_config "$ups" serial - get_write_ups_config "$ups" shutdown_delay - get_write_ups_config "$ups" snmp_version - get_write_ups_config "$ups" snmp_retries - get_write_ups_config "$ups" snmp_timeout - get_write_ups_config "$ups" synchronous - get_write_ups_config "$ups" usd - get_write_ups_config "$ups" vendor - get_write_ups_config "$ups" vendorid - - # Params specific to NetXML driver - get_write_ups_config "$ups" login - get_write_ups_config "$ups" password - get_write_ups_config "$ups" subscribe 0 1 - - # shellcheck disable=SC2317 - defoverride() { - local overvar="$1" - local defover="$2" - local overtype - local overval - - overtype="$(echo "$overvar" | tr '_' '.')" - - config_get overval "${defover}_${overvar}" value - [ -n "$overval" ] && echo "${defover}.${overtype} = $overval" >>"$UPS_C" - } - - config_list_foreach "$ups" override defoverride override - config_list_foreach "$ups" default defoverride default - - other() { - # shellcheck disable=SC2317 - local othervar="$1" - # shellcheck disable=SC2317 - local othervarflag="$2" - # shellcheck disable=SC2317 - local otherval - - # shellcheck disable=SC2317 - if [ "$othervarflag" = "otherflag" ]; then - config_get_bool otherval "${othervarflag}_${othervar}" value - [ "$otherval" = "1" ] && echo "${othervar}" >>"$UPS_C" - else - config_get otherval "${othervarflag}_${othervar}" value - [ -n "$otherval" ] && echo "${othervar} = $otherval" >>"$UPS_C" - fi - } - - config_list_foreach "$ups" other other other - config_list_foreach "$ups" otherflag other otherflag - echo "" >>$UPS_C - haveupscfg=1 -} - -build_global_driver_config() { - local cfg="$1" - - # Global driver config - get_write_ups_config "$cfg" chroot - get_write_ups_config "$cfg" driverpath - get_write_ups_config "$cfg" maxstartdelay - get_write_ups_config "$cfg" maxretry - get_write_ups_config "$cfg" retrydelay - get_write_ups_config "$cfg" pollinterval - get_write_ups_config "$cfg" synchronous - config_get runas "$cfg" user nut - RUNAS="$runas" - - echo "" >>"$UPS_C" -} - -build_config() { - local STATEPATH=/var/run/nut - - mkdir -p "$(dirname "$UPS_C")" - rm -f "$UPS_C" - echo "# Config file automatically generated from UCI config" > "$UPS_C" - chmod 0640 "$UPS_C" - - config_load nut_server +restore_umask_on_exit - srv_runas - srv_statepath - - [ -d "${STATEPATH}" ] || { - mkdir -p "${STATEPATH}" - } - chmod 0770 "${STATEPATH}" - - if [ -n "$RUNAS" ]; then - chown root:"$(id -gn "$RUNAS")" "${STATEPATH}" - fi - - SRV_RUNAS="$RUNAS" - config_foreach build_global_driver_config driver_global - if [ "$SRV_RUNAS" != "$RUNAS" ]; then - echo "WARNING: for proper communication drivers and server must 'runas' the same user" | logger -t nut-server - fi - - config_foreach build_ups_config driver - - build_server_config - [ -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 a ups driver instance start_ups_driver() { local ups="$1" local requested="$2" - local driver - local STATEPATH=/var/run/nut - local RUNAS=nut + local driver="" - # If wanting a specific instance, only start it - if [ "$requested" != "$ups" ] && [ "$requested" != "" ]; then + # If wanting a specific instance, only start that instance + if [ -n "$requested" ] && [ "$requested" != "$ups" ]; then return 0 fi - # Avoid hotplug inadvertently restarting driver during - # forced shutdown - [ -f /var/run/killpower ] && return 0 - if [ -d /var/run/nut ] && [ -f /var/run/nut/disable-hotplug ]; then - return 0 - fi - - # Depends on config_load from start_service - srv_statepath - srv_runas - ensure_usb_ups_access "$ups" + # For non-USB devices this is effectively a no-op + # For USB devices, if the USB productid and USB vendorid of the configured + # driver instance (UPS) match a USB device in sysfs, set the permissions + # on the USB device node in /dev/bus/usb/... to allow NUT to access and + # control the device. + ensure_usb_ups_access "$ups" || return 1 + config_get driver "$ups" driver + + [ -n "$driver" ] || { + log_error "Missing driver when starting UPS '$ups'" "nut-server" "nut-server" + return 1 + } - config_get driver "$ups" driver "usbhid-ups" procd_open_instance "$ups" procd_set_param respawn - procd_set_param stderr 1 - procd_set_param stdout 0 # Subset of stderr + procd_set_param stderr 2 + procd_set_param stdout 1 # Messages on stdout are a subset of those on stderr procd_set_param env NUT_QUIET_INIT_UPSNOTIFY=true procd_set_param env NUT_STATEPATH="${STATEPATH}" - procd_set_param command /usr/libexec/nut/"${driver}" -FF -a "$ups" ${RUNAS:+-u "$RUNAS"} + procd_set_param command /usr/libexec/nut/"${driver}" + procd_append_param command -FF -a "$ups" + if [ -n "$RUNAS" ]; then + procd_append_param command -u "$RUNAS" + fi procd_close_instance - haveupscfg=1 } -interface_triggers() { - local action="$1" - local triggerlist trigger - - config_get triggerlist upsd triggerlist +have_upsd_section() { + have_upsd_section="true" +} - # shellcheck disable=SC1091 - . /lib/functions/network.sh +common_preconditions() { + # No config, or error loading config is fatal + config_load nut_server || { + log_config_load_error "nut_server" "nut-server" "nut-server" + exit 1 + } - if [ -n "$triggerlist" ]; then - for trigger in $triggerlist; do - if [ "$action" = "add_trigger" ]; then - procd_add_interface_trigger "interface.*" "$trigger" /etc/init.d/nut-server reload - else - network_is_up "$trigger" && return 0 - fi - done + if have_section_of_type "upsd"; then + # Only find statepath and runas once per service start or reload + # defines STATEPATH + find_statepath "upsd" "nut_server" || { + log_error_exit "Failed to determine STATEPATH" "nut-server" "nut-server" + } + # sets RUNAS + find_runas "upsd" "nut_server" || { + log_error "Failed to determine RUNAS" "nut-server" "nut-server" + return 1 + } else - if [ "$action" = "add_trigger" ]; then - procd_add_raw_trigger "interface.*.up" 2000 /etc/init.d/nut-server reload - else - ubus call network.device status | grep -q '"up": true' && return 0 + # If there is no 'upsd' section in the nut_server config file, use + # default settings + STATEPATH=/var/run/nut + RUNAS=nut + fi + + # NUT_KILLPOWER is an external sentinel that indicates a forced shutdown + if [ -f "$NUT_KILLPOWER" ]; then + return 1 + fi + + if ! build_config; then + return 1 + fi +} + +stop_no_longer_configured_instances() { + local have_driver_instance=false + local have_upsd_instance=false + local instance instances + + # Stop any driver instances which are no longer configured + # We can only reliably do this for instances managed by procd + set -f + instances="$(list_running_instances "nut-server")" + [ -n "$instances" ] || return 0 + for instance in $instances; do + if [ "$instance" = "upsd" ]; then + continue + fi + config_get driver "$instance" driver + # Only stop not configured but running instances + if [ -z "$driver" ] && procd_running "nut-server" "$instance" >/dev/null 2>&1; then + procd_kill "nut-server" "$instance" 2>&1 | logger -s -t nut-server + fi + done + set +f + + # Need to stop drivers before stopping upsd + # The second loop is required in case some instances failed to stop in the + # first loop (so we cannot just track instances in the above loop). + # In addition, it is possible for hotplug triggered drivers start to start + # instances between the stop above, and this loop. + set -f + instances="$(list_running_instances "nut-server")" + [ -n "$instances" ] || return 0 + for instance in $instances; do + if [ "$instance" = "upsd" ]; then + have_upsd_instance="true" + continue fi + have_driver_instance="true" + break + done + set +f + + # If we have no UPS instances we must stop upsd or it will crash + # The "nut-server" service remains active and will 'see' configuration + # changes and execute reload_service when they are detected + if [ "$have_upsd_instance" = "true" ] && [ "$have_driver_instance" = "false" ]; then + # stop_server_instance + signal_instance "upsd" "upsd" "stop" "TERM" "${STATEPATH}/upsd.pid" "" "nut-server" "procd_kill" "nut-server" "upsd" fi - [ "$action" = "add_trigger" ] || return 1 } -start_server_instance() { - local srv="$1" +stop_all_instances() { + local instance instances - procd_open_instance "$srv" - procd_set_param respawn - procd_set_param stderr 1 - procd_set_param stdout 0 # Subset of stderr - procd_set_param env NUT_QUIET_INIT_UPSNOTIFY=true - procd_set_param env NUT_STATEPATH="$STATEPATH" - procd_set_param command /usr/sbin/upsd -FF ${RUNAS:+-u "$RUNAS"} - procd_close_instance + set -f + instances="$(list_running_instances "nut-server")" + [ -n "$instances" ] || return 0 + for instance in $instances; do + # stop upsd last + if [ "$instance" = "upsd" ]; then + continue + fi + config_get driver "$instance" driver + [ -n "$driver" ] || continue + + signal_instance "$instance" "/usr/libexec/nut/${driver}" "stop" "TERM" \ + "${STATEPATH}/${driver}-${instance}.pid" "" "nut-server" "procd_kill" "nut-server" "$instance" + done + set +f + # stop_server_instance + signal_instance "upsd" "upsd" "stop" "TERM" "${STATEPATH}/upsd.pid" "" "nut-server" "procd_kill" "nut-server" "upsd" + return 0 } -# shellcheck disable=SC2120 -start_service() { - local STATEPATH=/var/run/nut - local haveupscfg=0 - local havesrvcfg=0 - - # Avoid hotplug inadvertently restarting driver during - # forced shutdown - [ -f /var/run/killpower ] && return 0 - - srv_statepath - config_load nut_server - build_config - - should_start_srv=1 - [ "$havesrvcfg" = "1" ] || should_start_srv=0 - # Avoid crashloop on server (upsd) when no ups is configured; make sure server - # is not running if no ups is found in configuration - [ "$haveupscfg" = "1" ] || should_start_srv=0 - interface_triggers "check_interface_up" || should_start_srv=0 - - [ "$should_start_srv" = "1" ] || return 0 - - # We only start one service (upsd or one driver) from a given invocation - case "$1" in - "") - config_foreach start_ups_driver driver - start_server_instance upsd +service_preconditions() { + local action="$1" + local should_stop_srv=false + local should_start_srv=true + + case "$action" in + start) + common_preconditions || { + should_start_srv=false + } + interface_triggers "check_interface_up" || should_start_srv=false ;; - *upsd*) - start_server_instance upsd + reload) + common_preconditions || { + should_stop_srv=true + return 1 + } + interface_triggers "check_interface_up" || should_stop_srv=true ;; *) - config_foreach start_ups_driver driver "$1" + log_error "Unknown action '$action' in service_preconditions" "nut-server" "nut-server" + return 1 ;; esac -} - -server_active() { - local nut_server_active - - nut_server_active=$(_procd_ubus_call list | jsonfilter -l 1 -e "@['nut-server']") - [ "$nut_server_active" = "{ }" ] && return 0 -} -list_running_instances() { - local service="$1" - local running_instances + # If server not configured or no interfaces are up, diagnostic should already have been logged. + # Do not spam log more, but remove config and exit. + if [ "$should_start_srv" = "false" ]; then + remove_var_config + return 1 + fi - running_instances=$(_procd_ubus_call list | jsonfilter -e "@['$service'][@.*.running=true]") + # Stop all instances, remove config, and exit with error, if should_stop_srv is true + if [ "$should_stop_srv" = "true" ]; then + stop_all_instances || true + remove_var_config + return 1 + fi - if [ -n "$running_instances" ]; then - json_init - json_load "$running_instances" - json_get_keys instance_names - # shellcheck disable=SC2154 - echo "$instance_names" - json_cleanup + # NUT_KILLPOWER is an external sentinel that may be set between now and + # the previous check + if [ -f "$NUT_KILLPOWER" ]; then + return 1 fi + + # If we're here we should have good new config; activate it + mv -f "$USERS_C.new" "$USERS_C" || return 1 + mv -f "$UPS_C.new" "$UPS_C" || return 1 + mv -f "$UPSD_C.new" "$UPSD_C" || return 1 + return 0 } -signal_instance() { - local instance_name="$1" - local process_name="$2" - local signal_command="$3" - local signal="$4" - local pidfile="$5" - local secondary_command="$6" - - if [ -s "$pidfile" ]; then - $signal_command | logger -t nut-server - elif pgrep "$process_name" >/dev/null 2>/dev/null; then - procd_send_signal nut-server "$instance_name" "$signal" 2>&1 | logger -t nut-server +# Start upsd instance +start_server_instance() { + if [ -f "$NUT_KILLPOWER" ]; then + return 1 fi - if [ -n "$secondary_command" ] && procd_running nut-server "$instance_name"; then - $secondary_command 2>&1 | logger -t nut-server + + procd_open_instance upsd + procd_set_param respawn + procd_set_param stderr 2 + procd_set_param stdout 1 # Messages on stdout are a subset of stderr + procd_set_param env NUT_QUIET_INIT_UPSNOTIFY=true + procd_set_param env NUT_STATEPATH="$STATEPATH" + procd_set_param command /usr/sbin/upsd + procd_append_param command -FF + if [ -n "$RUNAS" ]; then + procd_append_param command -u "$RUNAS" fi + procd_close_instance } +remove_var_config() { + for rm_file in "$USERS_C" "$UPS_C" "$UPSD_C" \ + "$USERS_C.new" "$UPS_C.new" "$UPSD_C.new"; do + rm -f "$rm_file" + done +} + +# Stop NUT drivers and upsd (NUT server) +# We do not error exit (only log issues) because we do not want to prevent +# processing of further drivers. stop_ups_driver() { local ups="$1" # The ups (driver instance) local requested="$2" local driver # If wanting a specific instance, only stop it - if [ "$requested" != "$ups" ] && [ "$requested" != "" ]; then + if [ -n "$requested" ] && [ "$requested" != "$ups" ]; then return 0 fi - srv_statepath - build_ups_config "$ups" + config_get driver "$ups" driver - # If we don't have UPS configuration simply stop all instances - if [ "$haveupscfg" != "1" ]; then - if procd_running nut-server '*' >/dev/null 2>&1; then - procd_kill nut-server '*' 2>&1 | logger -t nut-server - fi + if [ -z "$driver" ]; then + log_error "No driver specified for UPS '$ups'. Attempting procd_kill." "nut-server" "nut-server" + procd_kill "nut-server" "$ups" || log_error "No driver specified for UPS '$ups' and procd_kill fallback failed" "nut-server" "nut-server" return 0 fi - config_get driver "$ups" driver "usbhid-ups" - - if procd_running nut-server "$ups"; then - signal_instance "$ups" "$driver" "/usr/libexec/nut/${driver} -c exit -a ${ups}" "TERM" "${STATEPATH}/${driver}-${ups}.pid" - if procd_running nut-server upsd >/dev/null 2>&1; then - signal_instance upsd upsd "upsd -c stop" "TERM" "${STATEPATH}/upsd.pid" "procd_kill nut-server upsd" - fi + # If there is a running driver instance for this UPS + if procd_running "nut-server" "$ups" >/dev/null 2>&1; then + signal_instance "$ups" "/usr/libexec/nut/${driver}" exit "TERM" "${STATEPATH}/${driver}-${ups}.pid" "${ups}" "nut-server" fi + return 0 } +# Reload NUT drivers and upsd (NUT server) +# We do not error exit (only log issues) because we do not want to prevent +# processing of further drivers. reload_ups_driver() { local ups="$1" local requested="$2" local driver # If wanting a specific instance, only reload that instance - if [ "$requested" != "$ups" ] && [ "$requested" != "" ]; then + if [ -n "$requested" ] && [ "$requested" != "$ups" ]; then return 0 fi - # Avoid hotplug inadvertently restarting driver during - # forced shutdown - [ -f /var/run/killpower ] && return 0 - if [ -d /var/run/nut ] && [ -f /var/run/nut/disable-hotplug ]; then + if [ -f "$NUT_KILLPOWER" ]; then + stop_ups_driver "$ups" return 0 fi - config_get driver "$ups" driver "usbhid-ups" + config_get driver "$ups" driver - srv_statepath + # Try to reload, otherwise exit politely + if procd_running "nut-server" "$ups"; then + signal_instance "$ups" "/usr/libexec/nut/${driver}" "reload-or-exit" "HUP" "${STATEPATH}/${driver}-${ups}.pid" "${ups}" "nut-server" + # Fall through to start driver, if it exited due to 'reload-or-exit' + # finding a configuration condition which requires a stop and start + # for the configuration to be applied. + fi - # Try to reload, otherwise exit politely, then stop and restart procd instance - if procd_running nut-server "$ups"; then - signal_instance "$ups" "$driver" "/usr/libexec/nut/${driver} -c reload-or-exit -a {ups}" HUP "${STATEPATH}/${driver}-${ups}.pid" + # If the driver instance (UPS) was terminated, or stopped 'naturally', + # start it up again + if ! procd_running "nut-server" "$ups" >/dev/null 2>&1; then + start_ups_driver "$ups" "$ups" fi - /etc/init.d/nut-server start "$ups" 2>&1 | logger -t nut-server + return 0 } -reload_service() { - local should_stop_srv - local STATEPATH=/var/run/nut - local havesrvcfg=0 - local haveupscfg=0 - local running_instances - local driver - - # Avoid hotplug inadvertently restarting driver during forced shutdown - [ -f /var/run/killpower ] && return 0 - - config_load nut_server - build_config +stop_service_if_no_instances() { + if service_active_no_instances "nut-server"; then + logger -s -t "nut-server" "nut-server active with no instances" + # If "nut-server" is active with no instances + # We don't care about the exit code of procd_kill, and logging its + # stderr can aid debugging. + procd_kill "nut-server" 2>&1 | logger -s -t "nut-server" + fi +} - should_stop_srv=0 - [ "$havesrvcfg" = "1" ] || should_stop_srv=1 - # Avoid crashloop on server (upsd) when no ups is configured; make sure server - # is not running if no ups is found in configuration - [ "$haveupscfg" = "1" ] || should_stop_srv=1 - interface_triggers "check_interface_up" || should_stop_srv=1 +# Stop or reload active instances +manage_instances() { + local action="$1" - if [ "$should_stop_srv" != "0" ]; then - if procd_running nut-server upsd >/dev/null 2>&1; then - procd_kill nut-server upsd 2>&1 | logger -t nut-server + case "$action" in + start) + config_foreach start_ups_driver driver + start_server_instance + ;; + reload) + if service_active_no_instances "nut-server"; then + log_msg "nut-server active with no instances. Reloading." "nut-server" "nut-server" "info" fi - config_foreach stop_ups_driver driver - - # Also stop any driver instances which are no longer configured - for instance in $(list_running_instances "nut-server"); do - if [ "$instance" != "upsd" ] && procd_running nut-server "$instance" >/dev/null 2>&1; then - procd_kill nut-server "$instance" 2>&1 | logger -t nut-server - fi - done - fi - - # If nut-server was started but has no instances (even upsd) - if server_active; then - logger -t nut-server "nut-server active with no instances" - /etc/init.d/nut-server start 2>&1 | logger -t nut-server - # Otherwise, if we have at least one instance running - elif procd_running nut-server; then - # If server (upsd) is running - if procd_running nut-server upsd; then - # Try to signal server (upsd) to reload configuration - signal_instance "upsd" "upsd" "upsd -c reload" HUP "${STATEPATH}/upsd.pid" - # If server (upsd) is not running - else - # Start server (upsd) - /etc/init.d/nut-server start "upsd" 2>&1 | logger -t nut-server + if procd_running "nut-server" upsd >/dev/null 2>&1; then + signal_instance "upsd" "upsd" "reload" "HUP" "${STATEPATH}/upsd.pid" "" "nut-server" "procd_kill" "nut-server" "upsd" fi config_foreach reload_ups_driver driver - - # Stop any driver instances which are no longer configured - # We can only reliably do this for instances managed by procd - for instance in $(list_running_instances "nut-server"); do - if [ "$instance" = "upsd" ]; then - continue - fi - unset driver - config_get driver "$instance" driver - if [ -z "$driver" ] && procd_running nut-server "$instance" >/dev/null 2>&1; then - procd_kill nut-server "$instance" 2>&1 | logger -t nut-server - fi - done - # Nut-server is not started, so start it - else - /etc/init.d/nut-server start 2>&1 | logger -t nut-server - fi + ;; + stop) + stop_all_instances + stop_service_if_no_instances + remove_var_config + ;; + stop-instances) + stop_all_instances + remove_var_config + ;; + esac } -stop_service() { - config_load nut_server - srv_statepath - - # We only handle the first parameter passed - case "$1" in - "") - # If nut-server was started but has no instances (even upsd) - if server_active; then - logger -t nut-server "nut-server active with no instances" - procd_kill nut-server 2>&1 | logger -t nut-server - elif procd_running nut-server; then # if have at least one instance - signal_instance "upsd" "upsd" "upsd -c stop" TERM "${STATEPATH}/upsd.pid" "procd_kill nut-server upsd" - - config_foreach stop_ups_driver driver - - # Also stop any driver instances which are no longer configured - for instance in $(list_running_instances "nut-server"); do - if [ "$instance" != "upsd" ] && procd_running nut-server "$instance" >/dev/null 2>&1; then - procd_kill nut-server "$instance" 2>&1 | logger -t nut-server - fi - done - - # If nut-server active but has no instances (even upsd) - if server_active >/dev/null 2>&1; then - procd_kill nut-server 2>&1 | logger -t nut-server - fi - fi +# Manage NUT drivers and upsd (NUT server) services +manage_service() { + local action="$1" + shift || true + local instance="$1" + + case "$action" in + start) + # We only start one service (upsd or one driver) from a given invocation + case "$instance" in + "") + service_preconditions "$action" || return 1 + stop_no_longer_configured_instances + manage_instances start + ;; + upsd) + service_preconditions "$action" || return 1 + stop_no_longer_configured_instances + start_server_instance + ;; + *) + service_preconditions "$action" || return 1 + stop_no_longer_configured_instances + config_foreach start_ups_driver driver "$instance" + ;; + esac + ;; + reload) + service_preconditions "$action" || return 1 + stop_no_longer_configured_instances + manage_instances reload ;; - *upsd*) - if procd_running nut-server upsd; then - signal_instance "upsd" "upsd" "upsd -c stop" TERM "${STATEPATH}/upsd.pid" "procd_kill nut-server upsd" - # If nut-server is active with no instances - if server_active; then - procd_kill nut-server 2>&1 | logger -t nut-server + stop) + case "$instance" in + "") + manage_instances stop + ;; + upsd) + if procd_running "nut-server" upsd >/dev/null 2>&1; then + signal_instance "upsd" "upsd" "stop" "TERM" "${STATEPATH}/upsd.pid" "" "nut-server" "procd_kill" "nut-server" "upsd" fi - fi + stop_service_if_no_instances + ;; + *) + # We only handle the first parameter, so do not pass in all parameters + config_foreach stop_ups_driver driver "$instance" + stop_service_if_no_instances + ;; + esac ;; - *) - # We only handle the first parameter, so do not pass in all parameters - config_foreach stop_ups_driver driver "$1" + stop-instances) + case "$instance" in + "") + manage_instances stop-instances + ;; + upsd) + if procd_running "nut-server" upsd >/dev/null 2>&1; then + signal_instance "upsd" "upsd" "stop" "TERM" "${STATEPATH}/upsd.pid" "" "nut-server" "procd_kill" "nut-server" "upsd" + fi + ;; + *) + # We only handle the first parameter, so do not pass in all parameters + config_foreach stop_ups_driver driver "$instance" + ;; + esac ;; esac } -service_triggers() { - config_load nut_server +# Start NUT drivers and upsd (NUT server) +start_service() { + manage_service start "$@" +} + +# Reload NUT drivers and upsd (NUT server) +reload_service() { + manage_service reload "$@" +} - interface_triggers "add_trigger" +# Stop NUT drivers and upsd (NUT server) +stop_service() { + manage_service stop "$@" +} + +service_triggers() { + interface_triggers "add_trigger" "upsd" procd_add_reload_trigger "nut_server" } diff --git a/net/nut/files/nut-service.sh.functions b/net/nut/files/nut-service.sh.functions new file mode 100644 index 0000000000000..b1a629087f32e --- /dev/null +++ b/net/nut/files/nut-service.sh.functions @@ -0,0 +1,333 @@ +#!/bin/sh +# Shebang line included so editors and shellcheck/shfmt know this contains +# shell code + +# Service helper functions for NUT server initscript + +# In recent (relevant) versions of shellcheck busybox is a valid shell type +# shellcheck shell=busybox + +# Pre-requisite sourcing for functions this script uses +# * /lib/functions.sh has been sourced +# * /lib/functions/network.sh has been sourced +# * /lib/functions/nut/nut-common.sh has been sourced + +# /var/run/killpower is a system-level sentinel created by another program +# which indicates that the system should be powering off +# /var/run is typically a tmpfs on OpenWrt and is not persisted across reboots +NUT_KILLPOWER="/var/run/killpower" + +# Disable NUT hotplug path +NUT_DISABLE_HOTPLUG_PATH="/var/run/nut/disable-hotplug" + +# Fallback STATEPATH setting +NUT_BASE_STATEPATH="/var/run/nut" + +# STATEPATH and RUNAS values are not valid until after find_statepath +# and/or find_runas +STATEPATH="" +RUNAS="" + +# Delay between interface change and interface trigger reload action +PROCD_INTERFACE_RELOAD_DELAY=3000 + +allow_hotplug_restart() { + # Allow hotplug to restart driver only if no NUT forced shutdown + # (as indicated by /var/run/killpower) is in progress + # and hotplug is not disabled + + # NUT_KILLPOWER is a sentinel created by another program + # which indicates that the system should be powering off + [ ! -f "$NUT_KILLPOWER" ] || return 1 + + # Disable hotplug path; a sentinel created by config + # or nutshutdown + [ ! -f "$NUT_DISABLE_HOTPLUG_PATH" ] || return 1 + + return 0 +} + +# Emit list of running UPS service instances (empty if none) +# returns 0 if there are active instances, 1 if no instances are active +# 'service' (first positional parameter) must not come from user input, and is +# expected to be static values provided by developers +list_running_instances() { + local service="$1" + local running_instances service_filter + + check_safe_uci_name "$service" || return 1 + + service_filter="@['""$service""'][@.*.running=true]" + running_instances="$(_procd_ubus_call list | jsonfilter -e "$service_filter")" + + if [ -n "$running_instances" ]; then + json_init + json_load "$running_instances" || { + logger -s -t nut-service "json_load failed in list_running_instances" + return 1 + } + json_get_keys instance_names + # shellcheck disable=SC2154 + echo "$instance_names" + json_cleanup + return 0 + fi + return 1 +} + +# Send a signal to a UPS service instance +# **Not to be used with user input**, only use static information provided by +# developers as signal_command and secondary_command execute commands in the +# script's context +signal_instance() { + local instance_name="$1" + shift || true + local signal_command="$1" + shift || true + local signal_action="$1" + shift || true + local signal="$1" + shift || true + local pidfile="$1" + shift || true + local signal_extra_arg="$1" + shift || true + local service_name="$1" + shift || true + local secondary_command="$1" + shift || true + local process_name + + process_name=$(basename "$signal_command") + + # Prefer sending signal using '$signal_command', which requires that a + # PID file exists. Otherwise, send signal(s) to the process(es) by their + # instance name + if [ -s "$pidfile" ]; then + # Informational log message, not error + log_msg "Sending signal action '$signal_action' to '$signal_command' for '$instance_name'" "nut-service.sh" "$service_name" "info" + set -f + # **Not to be used with user input** + # shellcheck disable=SC2086 + if [ -n "$signal_extra_arg" ]; then + "$signal_command" -c "$signal_action" -a "$signal_extra_arg" 2>&1 | logger -s -t "$service_name" + else + "$signal_command" -c "$signal_action" 2>&1 | logger -s -t "$service_name" + fi + set +f + # Modern OpenWrt (since OpenWrt-24.10, which is old stable, two releases + # prior to the release targetted by this script) has pgrep with the expected + # functionality--it, however, cannot send signals to processes. + elif pgrep "$process_name" >/dev/null 2>/dev/null; then + # If 'process_name' is in the process table, signal it with procd_send_signal + # Informational log message, not error + log_msg "Sending signal '$signal' to '$instance_name'" "nut-service.sh" "$service_name" "info" + procd_send_signal "$service_name" "$instance_name" "$signal" 2>&1 | logger -s -t "$service_name" + else + # No matching instance/process, which is an allowed possible state. + : + fi + + # **Not to be used with user input** + if [ -n "$secondary_command" ] && procd_running "$service_name" "$instance_name"; then + # Informational log message, not error + logger -s -t "$service_name" "Performing '$secondary_command' $* for '$instance_name'" + set -f + "$secondary_command" "$@" 2>&1 | logger -s -t "$service_name" + set +f + fi + # No signal sent is a valid possibility. Also, if sending a signal fails + # we want to keep going, so don't error, but do log the condition. + return 0 +} + +config_foreach_get() { + local section="$1" + local option="$2" + local value + + # nut_found_value is a 'pseudo-global' variable so that the value can be + # passed back to the calling function (that is the variable is defined in + # that function's context, and modification in functions called from that + # function will update the variable there). + config_get value "$section" "$option" + if [ -n "$value" ]; then + nut_found_value="$value" + nut_found_value_count=$((nut_found_value_count + 1)) + fi + return 0 +} + +find_foreach_value() { + local section_type="$1" + local package="$2" + local option="$3" + local default="$4" + local nut_found_value="" + local nut_found_value_count=0 + + # 'pseudo-global' variable so that the value can be passed back to this function (that is the + # variable is defined in this function's context, and modification in functions called from + # this function will update variable here). + nut_value_via_foreach="" + + config_foreach config_foreach_get "$section_type" "$option" || { + log_error "config_foreach for config_foreach_get for '$package'.'$section_type' failed." nut-service.sh nut-service + # nut_found_value and nut_found_value_count are indeterminate on config_foreach failure + nut_found_value="" + nut_found_value_count=0 + return 1 + } + + if [ "$nut_found_value_count" -gt 1 ]; then + # Informational log messages, not error + logger -t nut-common "Found more than one '$option' setting in '$package' for '$section_type'." + logger -t nut-common "Using last found '$option': nut_found_value='$nut_found_value'" + fi + + if [ -z "$nut_found_value" ]; then + nut_found_value="$default" + fi + + nut_value_via_foreach="$nut_found_value" + return 0 +} + +# Store path for NUT working data in the global variable STATEPATH +# shellcheck disable=SC2329 +find_statepath() { + local section_type="$1" + local package="$2" + # Callers should not need to see the variable below outside this function + # Conversely, functions called by this function are able to see and set + # the variable below, which acts as a 'pseudo-global' to called functions. + local nut_value_via_foreach="" + + find_foreach_value "$section_type" "$package" statepath "$NUT_BASE_STATEPATH" || return 1 + # Modern OpenWrt (since at least OpenWrt-24.10 which is now old stable, we are developing for + # not yet released OpenWrt, so two versions later) supports readlink -f from busybox. + [ -n "$nut_value_via_foreach" ] || return 1 + [ -d "$(readlink -f "$nut_value_via_foreach" 2>/dev/null)" ] || return 1 + + # shellcheck disable=2034 + STATEPATH="$nut_value_via_foreach" + return 0 +} + +# Store user under which to run daemon processes +# shellcheck disable=SC2329 +find_runas() { + local section_type="$1" + local package="$2" + local fallback_runas="$3" + # Callers should not need to see the variable below outside this function + # Conversely, functions called by this function are able to see and set + # the variable below, which acts as a 'pseudo-global' to called functions. + local nut_value_via_foreach="" + + find_foreach_value "$section_type" "$package" runas "${fallback_runas:-nut}" || return 1 + [ -n "$nut_value_via_foreach" ] || return 1 + [ -n "$(id -un "$nut_value_via_foreach")" ] || { + log_error "User '$nut_value_via_foreach' specified for RUNAS does not exist." nut-service.sh nut-service + return 1 + } + + # shellcheck disable=2034 + RUNAS="$nut_value_via_foreach" + return 0 +} + +# Detect if service is running under procd, with no instances +# (e.g. upsd with no UPS drivers running) +service_active_no_instances() { + local service="$1" + local active_instances + local active_var service_filter + + check_safe_uci_name "$service" || return 1 + + service_filter="@['""${service}""']" + active_instances="$(_procd_ubus_call list "{\"name\":\"$service\"}" | jsonfilter -l 1 -e active_var="$service_filter")" + # If we get no output at all, the service is not running + [ -z "$active_instances" ] && return 1 + + case "$active_instances" in + export\ active_var=*\;\ ) + active_var="${active_instances#export active_var=}" + active_var="${active_var%%; }" + ;; + *) + log_error "Unexpected value '$active_instances' for 'active_instances' from jsonfilter in service_active_no_instances" nut-service.sh nut-service + return 1 + ;; + esac + + # active_var is only empty, but present, if there are no instances for the service + [ -z "$active_var" ] && return 0 + + return 1 +} + +# Setup triggers in procd for changes to specified network interfaces +# network_is_up depends on /lib/functions/network.sh having been sourced +interface_triggers() { + local action="$1" + local section="$2" + local interfaces interface + local have_up_interface + local trigger_failed + + config_get interfaces "$section" triggerlist + + # We list the actions we care about. Other actions are irrelevant. + # We do not need to log or otherwise 'notice' other actions + case "$action" in + add_trigger) + if [ -n "$interfaces" ]; then + set -f + trigger_failed="false" + # interfaces is deliberately unquoted so we get word-splitting for + # the for loop + for interface in $interfaces; do + # We use the variadic third and fourth parameters of procd_add_interface_trigger to + # restart instead of reload on interface up/down events. Additionally the variadic + # fourth and fifth parameters of procd_add_interface_trigger are passed through to + # the