diff --git a/net/nut/Makefile b/net/nut/Makefile index fa5acf83bf928..454779f15d22d 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. @@ -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:=2 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 @@ -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,9 +176,13 @@ define Package/nut-upsmon/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/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 $(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 @@ -193,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 @@ -299,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 @@ -329,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 @@ -373,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 @@ -383,9 +392,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 @@ -406,7 +415,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 +536,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 +567,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 \ @@ -579,7 +591,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/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 68d39f668b85b..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 OpenWrt.org -# +# 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-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 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 be1cd45e20a5c..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 OpenWrt.org +# 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 /lib/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" "/lib/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" "/lib/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