PKG_NAME:=nut
PKG_VERSION:=2.8.5
-PKG_RELEASE:=1
+PKG_RELEASE:=2
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://www.networkupstools.org/source/2.8/
$(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
# 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
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
is assigned a power value and a type (secondary or primary).
endef
+define Package/nut-upsmon/conffiles
+/etc/config/nut_monitor
+/etc/nut/upsmon.conf
+endef
+
define Package/nut-upsmon/install
$(INSTALL_DIR) $(1)/etc/nut
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_DIR) $(1)/etc/init.d
- $(INSTALL_DIR) $(1)/etc/uci-defaults
+ $(INSTALL_DIR) $(1)/lib/functions/nut
+ $(INSTALL_DIR) $(1)/usr/bin $(1)/etc/uci-defaults
$(INSTALL_BIN) ./files/nut-monitor.init $(1)/etc/init.d/nut-monitor
+ $(INSTALL_DATA) ./files/nut-monitor-config.sh.functions $(1)/lib/functions/nut/nut-monitor-config.sh
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/sbin/upsmon $(1)/usr/sbin/
$(INSTALL_BIN) ./files/nutshutdown $(1)/usr/sbin/nutshutdown
$(INSTALL_DATA) ./files/nut-monitor-migrate.default $(1)/etc/uci-defaults/80-nut-monitor-migrate
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
You can alternatively write your own script and save some space.
endef
+define Package/nut-upssched/conffiles
+/etc/nut/upssched.conf
+endef
+
define Package/nut-upssched/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_DIR) $(1)/usr/sbin
$(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-upssched/conffiles
endef
define Package/nut-web-cgi/conffiles
+/etc/uci-defaults/add_nut_httpd_conf
/etc/nut/hosts.conf
/etc/nut/upsset.conf
/etc/nut/upsstats.html
$(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
+++ /dev/null
-#!/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
+++ /dev/null
-"")
- [ -f /var/run/nut/disable-hotplug ] || \
- /etc/init.d/nut-server enabled && perform_libhid_action
- ;;
-esac
+++ /dev/null
-#!/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
-}
--- /dev/null
+#!/bin/sh
+
+# In recent (relevant) versions of shellcheck busybox is a valid shell type
+# shellcheck shell=busybox
+
+# 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
+
+# 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_INSTROOT" ] || 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"
+ exit 1
+ fi
+}
--- /dev/null
+#!/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
#!/bin/sh /etc/rc.common
# Copyright © 2012-2026 OpenWrt.org
-#
+
+# In recent (relevant) versions of shellcheck busybox is a valid shell type
+# shellcheck shell=busybox
+
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
#
+
+# shellcheck disable=SC2034
START=87
STOP=23
USE_PROCD=1
-DEFAULT=/etc/default/nut
-UPSCGI_C=/var/etc/nut/hosts.conf
-UPSCGI_S=/var/etc/nut/upsset.conf
+# Default is hardcoded in other program's configuration, as well as compile
+# time options for NUT CGI executable
+# /var is typically a tmpfs on OpenWrt and is not persisted across reboots
+UPSCGI_CONF_DIR=/var/etc/nut
+
+# Ephemeral configuration for configuring NUT hosts using CGI and upsset
+# (persisted using UCI)
+UPSCGI_UPSSET_CONF="${UPSCGI_CONF_DIR}/upsset.conf"
+# Ephemeral configuration for NUT hosts to display using CGI (persisted
+# using UCI)
+UPSCGI_HOSTS_CONF="${UPSCGI_CONF_DIR}/hosts.conf"
+
+# IPKG_INSTROOT is intentionally only set when building an image and
+# is intentionally empty on a live OpenWrt device
+
+# Shellcheck source paths intentionally point to the location of files of
+# the scripts in the development environment (where shellcheck is used), not
+# on the live OpenWrt device
+
+# shellcheck source=net/nut/files/functions.sh.functions
+. "${IPKG_INSTROOT}"/lib/functions.sh || {
+ # Before our sourcing error logging definitions are sourced
+ logger -s -t nut-cgi "Unable to source 'functions.sh'"
+ exit 1
+}
+
+# 'shellcheck' complains about nut-common.sh due to a case pattern it does not understand, even
+# though it is correctly POSIX compliant
+# shellcheck disable=SC1094
+# shellcheck source=net/nut/files/nut-common.sh.functions
+. "${IPKG_INSTROOT}"/lib/functions/nut/nut-common.sh || {
+ # Before our sourcing error logging definitions are sourced
+ logger -s -t nut-cgi "Failed to load nut-common.sh"
+ exit 1
+}
+
+restore_umask_on_exit
+
+ensure_conf_dir_exists() {
+ # If the directory already exists we presume the creator of the directory
+ # has set the correct permissions.
+ if [ ! -d "$UPSCGI_CONF_DIR" ]; then
+ # Make directory for the UPS CGI configuration files, if not present
+ # Set permission on dir we create for UPS CGI configuration files
+ # All NUT configuration files are here, some which must be readable
+ # by other binaries with different group and ownership (therefore must
+ # be world readable)
+ # NOTE: Ensure sensitive files have more restrictive permissions
+ umask 022
+ if ! mkdir -p "$UPSCGI_CONF_DIR"; then
+ restore_umask
+ return 1
+ fi
+ restore_umask
+ fi
+ return 0
+}
+
+set_upsset_conf_content() {
+ local upsset_conf_content="$1"
+
+ umask 133
+ if ! printf "%s" "$upsset_conf_content" >"$UPSCGI_UPSSET_CONF"; then
+ restore_umask
+ log_error "failed to configure upsset; disabling NUT CGI" nut-cgi nut-cgi
+ return 1
+ fi
+ restore_umask
+ return 0
+}
+
+disable_upsset() {
+ ensure_conf_dir_exists || {
+ log_error "failed to disable upsset due to invalid directory; disabling NUT CGI" nut-cgi nut-cgi
+ return 1
+ }
+ # Use embedded content in preference to previous symlink method to avoid an
+ # attack vector via modified symlink target
+ # Prefer variable to heredoc due to complication with if, then around a
+ # heredoc when using shellcheck and shfmt together.
+ set_upsset_conf_content "# Network UPS Tools - upsset.conf sample file for OpenWrt
+#
+# This file is provided to ensure that you do not expose your upsd server
+# to the world upon installing the CGI programs. Specifically, it keeps
+# the upsset.cgi program from running until you have assured it that you
+# have secured your web server's CGI directory.
+#
+# NOTE: Contents of this file should be pure ASCII (character codes
+# not in range would be ignored with a warning message).
+#
+###
+### I_HAVE_SECURED_MY_CGI_DIRECTORY
+###
+" || return 1
+ return 0
+}
-nut_upscgi_upsset() {
+configure_nut_upscgi_upsset() {
local cfg="$1"
local enable
config_get_bool enable "$cfg" enable 0
- if [ "$enable" -eq 1 ]; then
- ln -sf /etc/nut/upsset.conf.enable "$UPSCGI_S"
+ # Use embedded content in preference to previous symlink method to avoid an
+ # attack vector via modified symlink target
+ # Prefer variable to heredoc due to complication with if..then around a
+ # heredoc when using shellcheck and shfmt together.
+ [ "$enable" = "1" ] || {
+ # Logs an error in the disable_upsset function call if it fails
+ disable_upsset || return 1
+ # not enabled (disabled) is a valid configuration choice
+ return 0
+ }
+ if ensure_conf_dir_exists; then
+ # Leading newline is intentional
+ set_upsset_conf_content "
+I_HAVE_SECURED_MY_CGI_DIRECTORY
+" || {
+ upscgi_upsset_failed="true"
+ return 1
+ }
+ return 0
else
- ln -sf /etc/nut/upsset.conf.disable "$UPSCGI_S"
+ upscgi_upsset_failed="true"
+ # Logs an error in the disable_upsset function call if it fails
+ disable_upsset
+ return 1
fi
+ # No return 0 here as it would be dead code, and shellcheck would complain
+ # about the dead code.
}
-nut_upscgi_add() {
- local cfg="$1"
- local upsname
- local hostname
- local port
- local displayname
-
- config_get upsname "$cfg" upsname
- config_get hostname "$cfg" hostname localhost
- config_get port "$cfg" port
- config_get pass "$cfg" password
- system="$upsname@$hostname"
+log_validation_error() {
+ local var="$1"
+ local ups="$2"
+ log_error "Unsafe characters in '$var' for '$ups'; skipping" nut-cgi nut-cgi
+}
+
+validate_host() {
+ local ups="$1"
+ local upsname="$2"
+ local hostname="$3"
+ local port="$4"
+ local displayname="$5"
+
+ # We do not unset upscgi_hosts_exists on error as we do not want to block
+ # monitoring hosts with the needed configuration as a result of those that
+ # do not
+ for var in upsname hostname port displayname; do
+ case "$var" in
+ upsname)
+ if ! check_safe_uci_name "$upsname"; then
+ log_validation_error "$var" "$ups"
+ return 1
+ fi
+ ;;
+ hostname)
+ case "$hostname" in
+ "" | *[!a-zA-Z0-9.:-\[\]-]*)
+ log_validation_error "$var" "$ups"
+ return 1
+ ;;
+ esac
+ ;;
+ displayname)
+ # We accept a minimally useful set of characters for a display name
+ # We specifically omit characters like $ % " and '
+ # Some AI code reviewers get confused by \(\) and \space in the case
+ # pattern, but this is correct for busybox ash in modern OpenWrt
+ # and does not allow \ through the validation.
+ case "$displayname" in
+ *[!a-zA-Z0-9+!\(\)\ _=:.,-]*)
+ log_validation_error "$var" "$ups"
+ return 1
+ ;;
+ esac
+ ;;
+ port)
+ # We don't log missing port log, as port is optional (NUT defaults
+ # to using 3493 when it is not present)
+ if [ -n "$port" ]; then
+ case "$port" in
+ 0*)
+ log_validation_error "$var" "$ups"
+ return 1
+ ;;
+ *[!0-9]*)
+ log_validation_error "$var" "$ups"
+ return 1
+ ;;
+ esac
+ # Catches illegal port numbers, including negative port numbers
+ if [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
+ log_error "Port set, but not between 1-65535 for '$ups'" nut-cgi nut-cgi
+ return 1
+ fi
+ fi
+ ;;
+ esac
+ done
+ return 0
+}
+
+# Must be called from service_reload
+nut_upscgi_add_host() {
+ local ups="$1"
+ local upsname hostname port system displayname
+
+ config_get upsname "$ups" upsname "$ups"
+ config_get hostname "$ups" hostname localhost
+ config_get port "$ups" port
+ config_get displayname "$ups" displayname "$ups"
+
+ # We do not error exit as that would abort processing of other hosts
+ validate_host "$ups" "$upsname" "$hostname" "$port" "$displayname" || return
+
+ system="${upsname}@$hostname"
if [ -n "$port" ]; then
- system="$system:$port";
+ system="$system:$port"
+ fi
+
+ if ! printf "%s\n" "MONITOR $system \"$displayname\"" >>"${UPSCGI_HOSTS_CONF}.new"; then
+ log_error "Failed to add MONITOR to CGI host.conf for '$ups'" nut-cgi nut-cgi
+ # Failed printf could result in inconsistent UPSCGI_HOSTS_CONF.new state, so recreate it as
+ # empty so later hosts can still potentially be properly configured
+ if rm -f "${UPSCGI_HOSTS_CONF}.new"; then
+ return
+ else
+ # Cascade failure. Bail.
+ log_error_exit "Cascade failure in nut_upscgi_add_host. Bailing." nut-cgi nut-cgi
+ fi
fi
- config_get displayname "$cfg" displayname
- echo "MONITOR $system \"$displayname\"" >> "$UPSCGI_C"
+ upscgi_hosts_exists="true"
+ return 0
}
service_reload() {
- mkdir -m 0755 -p "$(dirname "$UPSCGI_C")"
- rm -f "$UPSCGI_C"
- rm -f "$UPSCGI_S"
+ # Callers should not need to see the variables below outside this function
+ # Conversely, functions called by this function are able to see and set
+ # the variables below, which act as 'pseudo-globals' to called functions.
+ local upscgi_hosts_exists="false"
+ local upscgi_upsset_failed="false"
+
+ config_load nut_cgi || {
+ log_config_load_error "nut_cgi" "nut-cgi" "nut-cgi"
+ exit 1
+ }
+
+ # NUT configuration does not exist if NUT CGI cannot be configured, so
+ # stop service and bailout.
+ ensure_conf_dir_exists || {
+ service_stop
+ return 1
+ }
- config_load nut_cgi
+ rm -f "${UPSCGI_HOSTS_CONF}.new"
+ umask 133
+ touch "${UPSCGI_HOSTS_CONF}.new" || {
+ restore_umask
+ service_stop
+ return 1
+ }
+ restore_umask
- config_foreach nut_upscgi_add host
- config_foreach nut_upscgi_upsset upsset
+ # We do not exit on error here as we do not want to block monitoring hosts
+ # with the needed configuration as a result of those that do not
+ config_foreach nut_upscgi_add_host host
- [ -s "$UPSCGI_C" ] && chmod 640 "$UPSCGI_C"
+ # If new host configuration was successfully created
+ if [ "$upscgi_hosts_exists" = "true" ] && [ -s "${UPSCGI_HOSTS_CONF}.new" ]; then
+ # Move the new configuration file to active use,
+ # removing the old configuration in the process
+ mv -f "${UPSCGI_HOSTS_CONF}.new" "${UPSCGI_HOSTS_CONF}"
+ elif [ "$upscgi_hosts_exists" = "true" ]; then
+ rm -f "${UPSCGI_HOSTS_CONF}.new"
+ else
+ # If no hosts are configured, use an empty hosts.conf (no configuration)
+ rm -f "${UPSCGI_HOSTS_CONF}"
+ rm -f "${UPSCGI_HOSTS_CONF}.new"
+ if ! touch "${UPSCGI_HOSTS_CONF}"; then
+ log_error "Failed to create empty hosts.conf" nut-cgi nut-cgi
+ return 1
+ fi
+ # Empty hosts is not necessarily an error, but we log for information
+ logger -s -t nut-cgi "No configured hosts"
+ fi
+
+ # NUT CGI host file configuration is independent of upsset configuration
+ config_foreach configure_nut_upscgi_upsset upsset
+ if [ "$upscgi_upsset_failed" = "true" ]; then
+ disable_upsset
+ return 1
+ fi
+ return 0
}
start_service() {
- service_reload
+ service_reload || return 1
+ return 0
}
reload_service() {
- service_reload
+ service_reload || {
+ stop_service
+ return 1
+ }
+ return 0
}
stop_service() {
- rm -f "$UPSCGI_C"
- rm -f "$UPSCGI_S"
- ln -sf /etc/nut/upsset.conf.disable "$UPSCGI_S"
+ # Remove the hosts configuration file
+ # Since this is only a configuration generation service, the only way to
+ # stop serving (without stopping the web server for all uses) is to remove
+ # the NUT CGI configuration
+ rm -f "$UPSCGI_HOSTS_CONF"
+ rm -f "$UPSCGI_HOSTS_CONF.new"
+
+ disable_upsset || return 1
+ return 0
}
service_triggers() {
- procd_add_reload_trigger "nut_cgi"
+ # Add a reload trigger on changes to the nut_cgi UCI config
+ procd_add_reload_trigger "nut_cgi" || return 1
+ return 0
}
--- /dev/null
+#!/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 max of 4 hex digits (1-max_length hex digits allowed) and min of 1
+ if [ "${#number}" -gt "$((max_length + 2))" ] || [ "${#number}" -lt 1 ]; 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
+}
--- /dev/null
+#!/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
+# * /lib/functions/nut/nut-service.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
+ # integer: negative values allowed
+ POLLFAIL_LOG_THROTTLE_MAX | \
+ OFFDURATION | \
+ OVERDURATION | \
+ OBLBDURATION)
+ config_get val "$cfg" "$uci_option"
+ case "${val#[-]}" in
+ *[!0123456789]*)
+ log_error "upsmon section '$cfg' bad value for '$uci_option'" nut-monitor-config.sh nut-monitor-config
+ return 1
+ ;;
+ '')
+ # Ignore options with no configuration
+ :
+ ;;
+ *)
+ if ! printf "%s %s\n" "$nut_option" "$val" >>"$config_file"; then
+ log_error "upsmon section '$cfg' failed to write '$nut_option'" nut-monitor-config.sh nut-monitor-config
+ return 1
+ fi
+ ;;
+ esac
+ ;;
+ # integer: negative values not allowed
+ DEADTIME | \
+ DEBUG_MIN | \
+ FINALDELAY | \
+ HOSTSYNC | \
+ MINSUPPLIES | \
+ NOCOMMWARNTIME | \
+ POLLFREQ | \
+ POLLFREQALERT | \
+ RBWARNTIME)
+ config_get val "$cfg" "$uci_option"
+ case "$val" in
+ *[!0123456789]*)
+ log_error "upsmon section '$cfg' bad value for '$uci_option'" nut-monitor-config.sh nut-monitor-config
+ return 1
+ ;;
+ '')
+ # Ignore options with no configuration
+ :
+ ;;
+ *)
+ if ! printf "%s %s\n" "$nut_option" "$val" >>"$config_file"; then
+ log_error "upsmon section '$cfg' failed to write '$nut_option'" nut-monitor-config.sh nut-monitor-config
+ 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"
+
+ RUNAS="${RUNAS:-nutmon}"
+ conf_group="$(id -gn "$RUNAS")"
+
+ if [ -z "$conf_group" ]; then
+ log_error "upsmon build_config failed to find group for the RUNAS user ('$RUNAS')" 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
+}
# shellcheck disable=SC2329,SC2317
process_upsmon_section() {
local cfg="$1"
- local notify_type flag_name notify_msg notify_section
+ local notify_type flag_name notify_msg notify_section val
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"
+ local nocomm_exists="false"
+
+ # Get hotsync (typo) value if present
+ config_get val "$cfg" hotsync
+ # Get hostsync value, only using hotsync as default if hostsync does not exist
+ config_get val "$cfg" hostsync "$val"
+
+ if [ -n "$val" ]; then
+ if ! uci -q set nut_monitor."$cfg".hostsync="$val"; then
+ log_migration_error "Failed to set hostsync value"
+ fi
+ fi
+
+ # Remove invalid hotsync option unconditionally
+ uci -q delete nut_monitor."$cfg".hotsync || true
+
+ if uci -q get nut_monitor.NOCOMM; then
+ nocomm_exists="true"
+ fi
# 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.
# 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)
+ # have been nocommmsg in previous script), preferring NOCOMM (correct
+ # version) if both exist
if [ "$notification_name" = "NOCOMMMS" ]; then
notification_name="NOCOMM"
fi
+ if [ "$notification_name" = "NOCOMM" ]; then
+ if [ "$nocomm_exists" = "true" ]; then
+ # Do not delete or overwrite pre-existing NOCOMM section
+ continue
+ fi
+ if uci -q get nut_monitor."$notification_name"; then
+ uci -q delete nut_monitor."$notification_name" || {
+ log_migration_error "Failed to remove duplicate NOCOMM 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
+ }
+ printf "'%s': WARN: '%s'\n" "nut-monitor-migrate" "Removed duplicate NOCOMM section, likely from NOCOMMS typo" >&2
+ fi
+ 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
#!/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=/dev/null
+. "${IPKG_INSTROOT}"/usr/share/libubox/jshn.sh || {
+ log_source_error "jshn.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
+# 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
+}
- [ ! -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
- }
+# /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
+}
- [ -s "$UPSMON_C" ] && chmod 640 "$UPSMON_C"
- [ -s "$UPSMON_C" ] && chgrp "$(id -gn "${runas:-nutmon}")" "$UPSMON_C"
+# 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
}
-interface_triggers() {
+restore_umask_on_exit
+
+service_preconditions() {
local action="$1"
- local triggerlist trigger
+ # action can be one of start, stop, or reload
- config_get triggerlist "upsmon" triggerlist
+ case "$action" in
+ start | reload)
+ # NUT_KILLPOWER is an external sentinel that indicates a forced shutdown
+ if [ -f "$NUT_KILLPOWER" ]; then
+ return 1
+ fi
- # shellcheck disable=SC1091
- . "${IPKG_INSTROOT}"/lib/functions/network.sh
+ config_load nut_monitor || {
+ log_config_load_error "nut_monitor" "nut-monitor" "nut-monitor"
+ 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-monitor restart
- else
- network_is_up "$trigger" && return 0
+ 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
+ log_msg "No upsmon section so no RUNAS user, falling back to nutmon" "nut-monitor" "nut-monitor" "notice"
+ 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 0 # stderr is just a dup of stdout + syslog
+ 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)
+ # We need to use rpc_procd to start an instance from service_reload
+ perform_instance_action "reload" "HUP" "rc_procd" "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"
+}
- 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"
+ # No config, or error loading config is fatal
+ config_load nut_monitor || {
+ log_config_load_error "nut_monitor" "nut-monitor" "nut-monitor"
+ exit 1
+ }
+
+ config_get interface_reload_delay upsmon interface_reload_delay $DEFAULT_PROCD_INTERFACE_RELOAD_DELAY
+
+ interface_triggers "add_trigger" "upsmon" || {
+ log_error_exit "Failed to add interface triggers" nut-monitor nut-monitor
+ }
procd_add_reload_trigger "nut_monitor"
}
--- /dev/null
+#!/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
#!/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
#!/bin/sh
-{
-exec /usr/sbin/sendmail root <<EOF
-From: root
+# In recent (relevant) versions of shellcheck busybox is a valid shell type
+# shellcheck shell=busybox
+
+# ## SAMPLE SCRIPT ##
+#
+# To be replaced by more tailored script created by user / admin
+
+# Example of a script for notification via sendmail, triggered by upsmon state
+# changes. Primarily a placeholder for a more tailored user-generated script.
+
+# 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 || {
+ logger -s -t nut-sendmail-notify "Unable to source 'functions.sh' in 'nut-sendmail-notify'. 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 || {
+ logger -s -t nut-sendmail-notify "Unable to source 'nut-common.sh' in 'nut-sendmail-notify'. Bailing"
+ exit 1
+}
+
+# function to log reason message was not sent
+log_send_failure_and_exit() {
+ local reason="$1"
+ local syslog_id="nut-sendmail-notify"
+ local message_prefix="Message was not sent"
+ logger -s -t "$syslog_id" "${message_prefix}: $reason" || {
+ printf "%s: %s: %s, and logging failed\n" "$syslog_id" "$message_prefix" "$reason" >&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
#!/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"
+# 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
+ }
+
+ # Only configure notifycmd to be nut-sendmail-notify if sendmail is present
+ if [ -n "$(command -v sendmail)" ]; then
+ # 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
+ }
fi
+
+ 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
--- /dev/null
+#!/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' 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" # Must be full path (e.g. /dev/...)
+ 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 "$devname" ]; then
+ if ! chgrp "$device_group" "$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 "$devname" ]; then
+ if ! chmod g+rw "$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 normal_devname
+
+ 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
+ # Normalize DEVNAME
+ normal_devname="$DEVNAME"
+ if [ -n "$DEVNAME" ] && [ "${DEVNAME#/dev/}" = "${DEVNAME}" ]; then
+ normal_devname="/dev/$DEVNAME"
+ fi
+ # Empty port from configuration means match any device.
+ if [ "$port" = "$normal_devname" ] || [ "$port" = "" ]; then
+ if nut_set_serial_port_permissions "$normal_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
--- /dev/null
+#!/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-service.sh has been sourced
+
+# * /lib/functions/nut/nut-common.sh must be source and find_statepath executed
+# before build_config is called
+
+# 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"
+ 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" ] || grep -q '^MODE=none' "$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 ('$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"
+
+ 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
+ elif [ "$haveupscfg" = "false" ]; then
+ log_msg "No configured UPS. Not starting upsd." nut-server-config.sh nut-server-config warn
+ return 2
+ fi
+ return 0
+}
--- /dev/null
+#!/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"
+ [ -n "$RUNAS" ] || {
+ log_error "No RUNAS user specified. Skipping ups '$ups'" nut-server-service.sh nut-server-service
+ return 1
+ }
+
+ 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[0-9]*/*' | 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
+}
#!/bin/sh /etc/rc.common
# Copyright © 2012-2026 OpenWrt.org
#
+# Startup script for NUT (UPS) server
+#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
#
-# shellcheck shell=ash
+# In recent (relevant) versions of shellcheck busybox is a valid shell type
+# shellcheck shell=busybox
# shellcheck disable=SC2034
START=70
STOP=30
-USERS_C=/var/etc/nut/upsd.users
-UPSD_C=/var/etc/nut/upsd.conf
-UPS_C=/var/etc/nut/ups.conf
-
USE_PROCD=1
-get_write_ups_config() {
- local ups="$1"
- local var="$2"
- local def="$3"
- local flag="$4"
- local val
-
- [ -z "$flag" ] && {
- config_get val "$ups" "$var" "$def"
- [ -n "$val" ] && [ "$val" != "0" ] && echo "$var = $val" >>"$UPS_C"
- }
+# IPKG_INSTROOT is intentionally only set when building an image and
+# is intentionally empty on a live OpenWrt device
- [ -n "$flag" ] && {
- config_get_bool val "$ups" "$var" "$def"
- [ "$val" = 1 ] && echo "$var" >>"$UPS_C"
- }
-}
+# The shellcheck source directives are used during development by external
+# tools to find the source files on the development system. They do not
+# affect runtime behaviour.
-srv_statepath() {
- local statepath
+# Also the filenames in the shellcheck source directives may differ from the
+# production names that are sourced on live OpenWrt devices.
- config_get statepath upsd statepath /var/run/nut
- STATEPATH="$statepath"
+# shellcheck source=net/nut/files/functions.sh.functions
+. "${IPKG_INSTROOT}"/lib/functions.sh || {
+ # Before our sourcing error logging definitions are sourced
+ logger -s -t "nut-server" "FATAL: Unable to source 'functions.sh' in 'nut-server'"
+ exit 1
}
-srv_runas() {
- local runas
-
- [ -n "$RUNAS" ] && return 0
-
- config_get runas upsd runas nut
- RUNAS="$runas"
+# 'shellcheck' complains about nut-common.sh due to a case pattern it does not understand, even
+# though it is correctly POSIX compliant
+# shellcheck disable=SC1094
+# shellcheck source=net/nut/files/nut-common.sh.functions
+. "${IPKG_INSTROOT}"/lib/functions/nut/nut-common.sh || {
+ # Before our sourcing error logging definitions are sourced
+ logger -s -t "nut-server" "FATAL: Unable to source 'nut-common.sh' in 'nut-server'"
+ exit 1
}
-listen_address() {
- local srv="$1"
-
- config_get address "$srv" address "::1"
- config_get port "$srv" port
- # shellcheck disable=SC2154
- echo "LISTEN $address $port" >>"$UPSD_C"
+# shellcheck source=/dev/null
+. "${IPKG_INSTROOT}"/lib/functions/network.sh || {
+ log_source_error "network.sh" "nut-server" "nut-server"
+ exit 1
}
-srv_config() {
- local srv="$1"
- local maxage maxconn certfile runas statepath
-
- # Note runas support requires you make sure USB device file is readable by
- # the runas user
- config_get runas "$srv" runas nut
- RUNAS="$runas"
-
- config_get statepath "$srv" statepath /var/run/nut
- STATEPATH="$statepath"
-
- config_get maxage "$srv" maxage
- [ -n "$maxage" ] && echo "MAXAGE $maxage" >>"$UPSD_C"
-
- [ -n "$statepath" ] && echo "STATEPATH $statepath" >>"$UPSD_C"
-
- config_get maxconn "$srv" maxconn
- [ -n "$maxconn" ] && echo "MAXCONN $maxconn" >>"$UPSD_C"
-
- #NOTE: certs only apply to SSL-enabled version
- config_get certfile "$srv" certfile
- [ -n "$certfile" ] && echo "CERTFILE $certfile" >>"$UPSD_C"
-}
-
-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-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
}
-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=/dev/null
+. "${IPKG_INSTROOT}"/usr/share/libubox/jshn.sh || {
+ log_source_error "jshn.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
+# 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_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"
+# 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_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"
+restore_umask_on_exit
- config_load nut_server
+# Start a ups driver instance
+start_ups_driver() {
+ local ups="$1"
+ local requested="$2"
+ local driver=""
- srv_runas
- srv_statepath
+ # If wanting a specific instance, only start that instance
+ if [ -n "$requested" ] && [ "$requested" != "$ups" ]; then
+ return 0
+ fi
- [ -d "${STATEPATH}" ] || {
- mkdir -p "${STATEPATH}"
+ # 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
}
- chmod 0770 "${STATEPATH}"
+ procd_open_instance "$ups"
+ procd_set_param respawn
+ procd_set_param stderr 0 # stderr is just a dup of stdout + syslog
+ procd_set_param stdout 1
+ procd_set_param env NUT_QUIET_INIT_UPSNOTIFY=true
+ procd_set_param env NUT_STATEPATH="${STATEPATH}"
+ procd_set_param command "/usr/libexec/nut/${driver}"
+ procd_append_param command -FF -a "$ups"
if [ -n "$RUNAS" ]; then
- chown root:"$(id -gn "$RUNAS")" "${STATEPATH}"
+ procd_append_param command -u "$RUNAS"
fi
+ procd_close_instance
- 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
+ # If upsd is running, reload it to pick up new driver
+ if procd_running "nut-server" "upsd" >/dev/null 2>&1; then
+ signal_instance "upsd" "upsd" "reload" "HUP" "${STATEPATH}/upsd.pid" "" "nut-server"
+ else
+ start_server_instance
fi
-
- config_foreach build_ups_config driver
-
- build_server_config
- [ -n "$RUNAS" ] && chgrp "$(id -gn "$RUNAS")" "$UPS_C"
}
-ensure_usb_ups_access() {
+# On reload, we need to use rpc_procd to add an instance to an already running
+# service, as procd_open_service is not called by default when doing a
+# reload vs start
+add_ups_driver() {
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
+ local requested="$2"
+ rc_procd start_ups_driver "$ups" "$requested"
+}
- # 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
+have_upsd_section() {
+ have_upsd_section="true"
}
-# Must be called from start_service
-start_ups_driver() {
- local ups="$1"
- local requested="$2"
- local driver
- local STATEPATH=/var/run/nut
- local RUNAS=nut
+common_preconditions() {
+ local ret
- # If wanting a specific instance, only start it
- if [ "$requested" != "$ups" ] && [ "$requested" != "" ]; then
- return 0
+ # 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 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 there is no 'upsd' section in the nut_server config file, use
+ # default settings
+ STATEPATH=/var/run/nut
+ RUNAS=nut
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
+ # NUT_KILLPOWER is an external sentinel that indicates a forced shutdown
+ if [ -f "$NUT_KILLPOWER" ]; then
+ return 1
fi
- # Depends on config_load from start_service
- srv_statepath
- srv_runas
- ensure_usb_ups_access "$ups"
+ build_config
+ ret=$?
- 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 env NUT_QUIET_INIT_UPSNOTIFY=true
- procd_set_param env NUT_STATEPATH="${STATEPATH}"
- procd_set_param command /usr/libexec/nut/"${driver}" -FF -a "$ups" ${RUNAS:+-u "$RUNAS"}
- procd_close_instance
- haveupscfg=1
+ return $ret
}
-interface_triggers() {
- local action="$1"
- local triggerlist trigger
-
- config_get triggerlist upsd triggerlist
-
- # shellcheck disable=SC1091
- . /lib/functions/network.sh
-
- 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
- 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
+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" ] && [ -n "$instance" ] && 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
-}
-
-# 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
+ 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
- [ "$should_start_srv" = "1" ] || return 0
+ 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
+}
- # 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
+ local ret=0
+
+ case "$action" in
+ start)
+ common_preconditions
+ ret=$?
+
+ case $ret in
+ 0)
+ interface_triggers "check_interface_up" || should_start_srv=false
+ ;;
+ *)
+ should_start_srv=false
+ ;;
+ esac
;;
- *upsd*)
- start_server_instance upsd
+ reload)
+ common_preconditions
+ ret=$?
+
+ case $ret in
+ 0 | 2)
+ interface_triggers "check_interface_up" || should_stop_srv=true
+ ;;
+ *)
+ should_stop_srv=true
+ ;;
+ esac
;;
*)
- config_foreach start_ups_driver driver "$1"
+ log_error "Unknown action '$action' in service_preconditions" "nut-server" "nut-server"
+ ret=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 $ret
+ 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 0 # stderr is just a dup of stdout + syslog
+ procd_set_param stdout 1
+ procd_set_param env NUT_QUIET_INIT_UPSNOTIFY=true
+ procd_set_param env NUT_STATEPATH="$STATEPATH"
+ procd_set_param command /usr/sbin/upsd
+ procd_append_param command -FF
+ if [ -n "$RUNAS" ]; then
+ procd_append_param command -u "$RUNAS"
fi
+ procd_close_instance
+}
+
+remove_var_config() {
+ for rm_file in "$USERS_C" "$UPS_C" "$UPSD_C" \
+ "$USERS_C.new" "$UPS_C.new" "$UPSD_C.new"; do
+ rm -f "$rm_file"
+ done
}
+# Stop NUT drivers and upsd (NUT server)
+# We do not error exit (only log issues) because we do not want to prevent
+# processing of further drivers.
stop_ups_driver() {
local ups="$1" # The ups (driver instance)
local requested="$2"
local driver
# If wanting a specific instance, only stop it
- if [ "$requested" != "$ups" ] && [ "$requested" != "" ]; then
+ if [ -n "$requested" ] && [ "$requested" != "$ups" ]; then
return 0
fi
- srv_statepath
- build_ups_config "$ups"
+ config_get driver "$ups" driver
- # If we don't have UPS configuration simply stop all instances
- if [ "$haveupscfg" != "1" ]; then
- if procd_running nut-server '*' >/dev/null 2>&1; then
- procd_kill nut-server '*' 2>&1 | logger -t nut-server
- fi
+ if [ -z "$driver" ]; then
+ log_error "No driver specified for UPS '$ups'. Attempting procd_kill." "nut-server" "nut-server"
+ procd_kill "nut-server" "$ups" || log_error "No driver specified for UPS '$ups' and procd_kill fallback failed" "nut-server" "nut-server"
return 0
fi
- config_get driver "$ups" driver "usbhid-ups"
-
- if procd_running nut-server "$ups"; then
- signal_instance "$ups" "$driver" "/usr/libexec/nut/${driver} -c exit -a ${ups}" "TERM" "${STATEPATH}/${driver}-${ups}.pid"
- if procd_running nut-server upsd >/dev/null 2>&1; then
- signal_instance upsd upsd "upsd -c stop" "TERM" "${STATEPATH}/upsd.pid" "procd_kill nut-server upsd"
- fi
+ # If there is a running driver instance for this UPS
+ if procd_running "nut-server" "$ups" >/dev/null 2>&1; then
+ signal_instance "$ups" "/usr/libexec/nut/${driver}" exit "TERM" "${STATEPATH}/${driver}-${ups}.pid" "${ups}" "nut-server"
fi
+ return 0
}
+# Reload NUT drivers and upsd (NUT server)
+# We do not error exit (only log issues) because we do not want to prevent
+# processing of further drivers.
reload_ups_driver() {
local ups="$1"
local requested="$2"
local driver
# If wanting a specific instance, only reload that instance
- if [ "$requested" != "$ups" ] && [ "$requested" != "" ]; then
+ if [ -n "$requested" ] && [ "$requested" != "$ups" ]; then
return 0
fi
- # Avoid hotplug inadvertently restarting driver during
- # forced shutdown
- [ -f /var/run/killpower ] && return 0
- if [ -d /var/run/nut ] && [ -f /var/run/nut/disable-hotplug ]; then
+ if [ -f "$NUT_KILLPOWER" ]; then
+ stop_ups_driver "$ups"
return 0
fi
- config_get driver "$ups" driver "usbhid-ups"
+ config_get driver "$ups" driver
- srv_statepath
+ # Try to reload, otherwise exit politely
+ if procd_running "nut-server" "$ups"; then
+ signal_instance "$ups" "/usr/libexec/nut/${driver}" "reload-or-exit" "HUP" "${STATEPATH}/${driver}-${ups}.pid" "${ups}" "nut-server"
+ # Fall through to start driver, if it exited due to 'reload-or-exit'
+ # finding a configuration condition which requires a stop and start
+ # for the configuration to be applied.
+ fi
- # Try to reload, otherwise exit politely, then stop and restart procd instance
- if procd_running nut-server "$ups"; then
- signal_instance "$ups" "$driver" "/usr/libexec/nut/${driver} -c reload-or-exit -a ${ups}" HUP "${STATEPATH}/${driver}-${ups}.pid"
+ # If the driver instance (UPS) was terminated, or stopped 'naturally',
+ # start it up again
+ if ! procd_running "nut-server" "$ups" >/dev/null 2>&1; then
+ add_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
+ ;;
+ 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_no_longer_configured_instances
+ ;;
+ stop)
+ stop_all_instances
+ stop_service_if_no_instances
+ remove_var_config
+ ;;
+ stop-instances)
+ stop_all_instances
+ remove_var_config
+ ;;
+ esac
+}
- # 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
+# Manage NUT drivers and upsd (NUT server) services
+manage_service() {
+ local action="$1"
+ # If we have no second parameter, we use the "") case and affect all instances
+ shift || true
+ # We only consume a single additional parameter (if not acting on all
+ # instances, we act only on one instance).
+ local instance="$1"
+ local ret=0
+
+ case "$action" in
+ start)
+ case "$instance" in
+ "")
+ service_preconditions "$action"
+ ret=$?
+
+ stop_no_longer_configured_instances
+
+ if [ "$ret" = "0" ]; then
+ manage_instances start
+ else
+ return 1
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
+ ;;
+ # We only start one service (upsd or one driver) from a given invocation
+ upsd)
+ service_preconditions "$action"
+ ret=$?
+ if [ "$ret" = "0" ]; then
+ start_server_instance
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_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
+ stop_no_longer_configured_instances
+
+ return $ret
+ ;;
+ # We only start one service (upsd or one driver) from a given invocation
+ *)
+ service_preconditions "$action"
+ ret=$?
+ if [ "$ret" = "0" ]; then
+ config_foreach start_ups_driver driver "$instance"
fi
+ stop_no_longer_configured_instances
+
+ return $ret
+ ;;
+ esac
+ ;;
+ reload)
+ service_preconditions "$action"
+ ret=$?
+
+ if [ "$ret" != "1" ]; then
+ manage_instances reload
+ fi
+
+ stop_no_longer_configured_instances
+
+ if [ "$ret" = "1" ]; then
+ return 1
fi
;;
- *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
}
+# Start NUT drivers and upsd (NUT server)
+start_service() {
+ manage_service start "$@"
+}
+
+# Reload NUT drivers and upsd (NUT server)
+reload_service() {
+ manage_service reload "$@"
+}
+
+# Stop NUT drivers and upsd (NUT server)
+stop_service() {
+ manage_service stop "$@"
+}
+
service_triggers() {
- config_load nut_server
+ # No config, or error loading config is fatal
+ config_load nut_server || {
+ log_config_load_error "nut_server" "nut-server" "nut-server"
+ exit 1
+ }
+
+ config_get interface_reload_delay upsd interface_reload_delay $DEFAULT_PROCD_INTERFACE_RELOAD_DELAY
- interface_triggers "add_trigger"
+ interface_triggers "add_trigger" "upsd" "$interface_reload_delay"
procd_add_reload_trigger "nut_server"
}
--- /dev/null
+#!/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
+# * /usr/share/libubox/jshn.sh has been sourced
+# * /lib/functions/procd.sh has been sourced (automatic for initscripts)
+
+# /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=""
+
+# Default delay between interface change and interface trigger reload action
+DEFAULT_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() {
+ if [ "$#" -lt 7 ]; then
+ log_error "signal_instance called with too few parameters."
+ return 1
+ fi
+ local instance_name="$1"
+ local signal_command="$2"
+ local signal_action="$3"
+ local signal="$4"
+ local pidfile="$5"
+ local signal_extra_arg="$6"
+ local service_name="$7"
+ # We use shift here and below to allow use of additional variadic parameters
+ shift 7
+ local secondary_command="$1"
+ shift || true
+ local process_name ret
+
+ 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"
+ ret=$?
+ else
+ "$signal_command" -c "$signal_action"
+ ret=$?
+ 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"
+ ret=$?
+ else
+ # No matching instance/process, which is an allowed possible state.
+ :
+ fi
+
+ # **Not to be used with user input**
+ # For NUT exit code 0 is success, 1 is error, and other codes have other meanings
+ # We only want to issue secondary command if the primary command fails
+ if [ "$ret" = "1" ] && [ -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 interface_reload_delay="${3:-$DEFAULT_PROCD_INTERFACE_RELOAD_DELAY}"
+ 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 <script> (in this case /etc/init.d/nut-server), so the trigger action becomes
+ # /etc/init.d/nut-server restart "upsd").
+ procd_add_interface_trigger "interface.*.up" "$interface" /etc/init.d/nut-server restart "upsd" || trigger_failed="true"
+ done
+ set +f
+ if [ "$trigger_failed" = "true" ]; then
+ return 1
+ fi
+ else
+ # Add a restart trigger on any interface's up status changing
+ procd_add_raw_trigger "interface.*.up" "$interface_reload_delay" /etc/init.d/nut-server restart || return 1
+ fi
+ ;;
+ check_interface_up)
+ if [ -n "$interfaces" ]; then
+ set -f
+ have_up_interface="false"
+ for interface in $interfaces; do
+ network_is_up "$interface" && have_up_interface="true" && break
+ done
+ set +f
+ if [ "$have_up_interface" = "false" ]; then
+ log_msg "None of the trigger interfaces are up." "nut-service.sh" "nut-service" "notice"
+ return 1
+ fi
+ else
+ # Return true if any interface is up
+ # -q for jsonfilter only suppress error output, not messages on
+ # stdout, redirect to >/dev/null is needed to avoid all output
+ if ubus -S call network.device status | jsonfilter -q -l 1 -e '$[@.up=true].up' >/dev/null; then
+ return 0
+ fi
+ log_msg "No interfaces are up." "nut-service.sh" "nut-service" "notice"
+ return 1
+ fi
+ ;;
+ esac
+ return 0
+}
#config host
# option upsname upsname
# option hostname localhost
-# option port # optional port number
+# option port <number> # optional port number
# option displayname "Display Name"
config upsset
-#config upsmon 'upsmon'
+# **NOTE**: Due to the presence of passwords in this file, restrictive file
+# permissions on this file in actual use in /etc/config/nut_monitor should be
+# used. For example:
+# chown root:root /etc/config/nut_monitor
+# chmod 0600 /etc/config/nut_monitor
+
+# We only support one instance of upsmon, and its UCI
+# section must be named 'upsmon'
+
+# defaults for the upsmon section are compile time defaults
+# except /path/to/dir values which have no default
+# and defaultnotify which is an UCI specific setting
+#config upsmon upsmon
# option runas nutmon
# option deadtime 15
# option finaldelay 5 # final delay
# option certverify 0
# option forcessl 0
# option debugmin 0
-# option defaultnotify SYSLOG
+# option defaultnotify SYSLOG
+# option interface_reload_delay 3000 # in milliseconds
#config monitor
# option type primary
# option username upsuser
# option password upspassword
+# notification config sections must be named with an uppercase valid
+# NUT event type, from the list at
+# https://networkupstools.org/docs/man/upsmon.conf.html
+#
+# Empty notifications sections will emit a default NOTIFYFLAG <event> SYSLOG
+# line, as otherwise the NUT default of SYSLOG+WALL would be used, and the
+# `wall` command is not present in OpenWrt by default
+
#config notifications 'COMMOK'
# option message "Communications with UPS %s established"
# option flag "SYSLOG"
# option flag "SYSLOG+EXEC"
#config notifications 'ONLINE'
-# option message "UPS %s is on line power"
-# option flag "SYSLOG"
+# option flag "IGNORE"
#config notifications 'ONBATT'
# option message "UPS %s is on battery power"
# option flag "SYSLOG+EXEC"
+
+#config notifications 'SHUTDOWN'
+# option message "UPS %s shutting down"
+# option flag "SYSLOG+EXEC"
+++ /dev/null
-#!/bin/sh
-
-nut_serial() {
- local cfg="$cfg"
- local runas enable_usb_serial port
- config_get_bool enable_usb_serial "$cfg" enable_usb_serial 0
- config_get port "$cfg" port
- config_get runas "$cfg" runas "nut"
-
- [ -z "$runas" ] && config_get runas upsd runas "nut"
-
- [ "$enable_usb_serial" -eq 1 ] && {
- # If port is specified only change tty's matching port
- if [ -n "$port" ] && [ "$port" != /dev/"$DEVNAME" ]; then
- return 0
- fi
- [ -n "$runas" ] && chgrp "$(id -gn "${runas}")" /dev/"$DEVNAME"
- chmod g+rw /dev/"$DEVNAME"
- }
-}
-
-nut_on_hotplug_add() {
- . "${IPKG_INSTROOT}"/lib/functions.sh
-
- config load nut_server
- config_foreach nut_serial driver
-}
-
-[ "$ACTION" = "add" ] && [ -n "$DEVNAME" ] && {
- # On add of a serial port with name ttyUSB*
- [ -z "${DEVNAME%ttyUSB*}" ] && nut_on_hotplug_add
- # On add of a serial port with name ttyAMA*
- [ -z "${DEVNAME%ttyAMA*}" ] && nut_on_hotplug_add
-}
#config driver_global 'driver_global'
-# option user nut
+# option driverpath /usr/libexec/nut # compile-time default
#config driver 'upsname'
-# option driver usbhid-ups
-# option port auto
-# option enable_usb_serial 0
+# option driver usbhid-ups # example
+# option port auto # example
+# option enable_usb_serial 0 # example
+#
+# ### NOTE ###
+# For USB UPSes it is necessary to properly configure
+# productid and vendorid (and serial number if you have
+# more than one device with the same productid and vendorid) in
+# nut_server so that the hotplug script can give NUT
+# permissions to access the UPS device(s)
#config user
# option username upsuser
# option upsmon secondary|primary
#config listen_address
-# option address ::1
-# option port 3493
+# option address ::1 # example
+# option port 3493 # compile-time default
+# defaults for the upsd section are compile time defaults
+# We only support one 'upsd' section and it must be named 'upsd'
#config upsd 'upsd'
# option maxage 15
# option statepath /var/run/nut
# option maxconn 1024
# option runas nut
+# option interface_reload_delay 3000 # in milliseconds
# NB: certificates only apply to SSL-enabled version
# option certfile /usr/local/etc/upsd.pem
#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
-#
-stop_instance() {
- /etc/init.d/nut-server stop "$1"
+# In recent (relevant) versions of shellcheck busybox is a valid shell type
+# shellcheck shell=busybox
+
+# Script called by NUT, as root, to safely shutdown the system in the event of
+# impending loss of power due to low battery discharge during a power outage,
+# or due to a FSD (Forced ShutDown) command.
+
+# Path to NUT server UCI configuration file
+NUT_SERVER_CONFIG=/etc/config/nut_server
+# Path to NUT drivers
+NUT_DRIVER_PATH=/usr/libexec/nut
+# Path to file created by NUT indicating a forced shutdown (FSD) is needed
+# On OpenWrt this will (intentionally) normally not exist after a reboot
+# as /var/run is typically a tmpfs
+NUT_KILLPOWER=/var/run/killpower
+# Disable NUT hotplug path
+NUT_DISABLE_HOTPLUG_PATH=/var/run/nut/disable-hotplug
+# Path to NUT server (de)initscript
+NUT_SERVER_INIT=/etc/init.d/nut-server
+# Delay for FSD to succeed before forcing shutdown
+# 2 minutes to allow UPS offdelay + safe shutdown grace period
+# Not a configuration option as reading config could fail
+NUT_FSD_FAIL_DELAY=120
+
+MISSING_CONFIG_BAIL_MESSAGE="No nut server config found while doing 'nutshutdown'. Bailing"
+MISSING_LIB_FUNCTIONS_SH_BAIL_MESSAGE="/lib/functions.sh is missing. Bailing"
+MISSING_UPS_INSTANCE_SKIP_MESSAGE="stop_nut_server_instance called with no ups (section) name. Skipping."
+NOT_A_LIVE_SYSTEM_BAIL_MESSAGE="Trying to use 'nutshutdown' when not on a live OpenWrt system. Bailing."
+UNSAFE_UPS_INSTANCE_NAME_SKIP_MESSAGE="No valid UPS UCI section name in shutdown_actual_ups. Skipping."
+
+# Only enact killpower (FSD: Forced Shut Down) if certain conditions are met
+DO_KILLPOWER="0"
+
+# shellcheck disable=SC2329,SC2317
+check_safe_name() {
+ case "$1" in
+ *[!a-zA-Z0-9_-]* | "")
+ return 1
+ ;;
+ *)
+ return 0
+ ;;
+ esac
+}
+
+# shellcheck disable=SC2329,SC2317
+stop_nut_server_instance() {
+ # We're in an emergency shutdown; best effort shutdown and don't block
+ if [ -z "$1" ]; then
+ echo "$MISSING_UPS_INSTANCE_SKIP_MESSAGE" >&2
+ logger -s -t nut-shutdown "$MISSING_UPS_INSTANCE_SKIP_MESSAGE" || true
+ return 1
+ fi
+ "$NUT_SERVER_INIT" stop "$1" || true
+}
+
+stop_drivers() {
+ if [ "$DO_KILLPOWER" = "1" ]; then
+ # FSD may not work unless drivers are stopped; try to stop them but
+ # don't block on failure; partial success would be better than none
+ config_foreach stop_nut_server_instance driver || true
+ fi
}
-shutdown_instance() {
- local cfg="$1"
- config_get driver "$cfg" driver "usbhid-ups"
+# shellcheck disable=SC2329,SC2317
+shutdown_actual_ups() {
+ local ups="$1"
+ local driver nut_ups_driver_full_path
+
+ # Avoid potentially blocking errors if "$ups" is somehow not defined
+ # with a proper UCI name (should never happen, hence skipping if it does)
+ if ! check_safe_name "$ups"; then
+ echo "$UNSAFE_UPS_INSTANCE_NAME_SKIP_MESSAGE" >&2
+ logger -s -t nut-shutdown "$UNSAFE_UPS_INSTANCE_NAME_SKIP_MESSAGE" || true
+ return 1
+ fi
+
+ config_get driver "$ups" driver
+
+ # If we didn't get the current driver, skip to the next one (best effort)
+ [ -n "$driver" ] || return 0
+
+ # ensure driver name is 'safe'/valid. This not being true could be a
+ # simple mistake, so we do not bail on the shutdown, we just ignore this UPS.
+ check_safe_name "$driver" || return 0
+
+ nut_ups_driver_full_path="${NUT_DRIVER_PATH}/${driver}"
# Only FSD if killpower was indicated
- if [ -f /var/run/killpower ]; then
- /usr/libexec/nut/"${driver}" -a "$cfg" -k
+ if [ -f "$NUT_KILLPOWER" ]; then
+ # We're in an emergency shutdown; best effort shutdown and don't block
+ # Ideally this will also tell the UPS to poweroff after 'offdelay'
+ # We do this late in the sequence to minimize the chance the forced
+ # shutdown of the UPS (-k) will result in power loss before we
+ # finish
+ "$nut_ups_driver_full_path" -a "$ups" -k || true
fi
}
-do_fsd() {
- if [ -f /var/run/killpower ]; then
- # Only make FS readonly if we are doing an FSD
- mount -o remount,ro /overlay /overlay
- mount -o remount,ro / /
-
- . ${IPKG_INSTROOT}/lib/functions.sh
-
- if [ -f /etc/config/nut_server ]; then
- config_load nut_server
-
- # Can't FSD unless drivers are stopped
- config_foreach stop_instance driver
- # Driver will wait 'offdelay' before shutting down
- config_foreach shutdown_instance driver
- # So this can happen
- rm -f /var/run/killpower
- poweroff
- # And just in case
- sleep 120
- # Uh-oh failed to poweroff UPS
- reboot -f
- else
- poweroff
- fi
- else
- poweroff
+remount_filesystems_as_readonly() {
+ # Make FS readonly, if we are doing an FSD, but only on a best effort basis,
+ # do not stop shutdown if it does not succeed as power is going to be lost
+ # anyway. Also logging may already be stopped, so no point (and could hang
+ # shutdown) trying to log any errors
+ mount -o remount,ro /overlay /overlay || true
+ mount -o remount,ro / / || true
+}
+
+# Do power off with delay and then forced reboot
+do_forced_shutdown() {
+ # In case poweroff forks and returns (but ignore error code, since if
+ # poweroff does return 'something' is wrong and we should not rely on it)
+ poweroff || true
+ # And just in case (even if poweroff claims it worked, it may not. If
+ # poweroff works properly this will never be reached).
+ sleep "$NUT_FSD_FAIL_DELAY"
+ # Uh-oh failed to poweroff system, force power off (not shutdown scripts
+ # will be run)
+ poweroff -f || true
+ sleep 1
+ # It still failed; hard force a non-syncing reboot
+ # Requires root and /proc to still be mounted
+ echo 1 >/proc/sys/kernel/sysrq || true
+ # Will immediately reboot the system
+ # without syncing or unmounting your disks.
+ # We do this as there is no bypass and halt option
+ # 'o' is essentially ACPI poweroff, which may not help us in an emergency.
+ echo b >/proc/sysrq-trigger || true
+}
+
+# IPKG_INSTROOT is intentionally only set when building an image and
+# is intentionally empty on a live OpenWrt device
+if [ -n "${IPKG_INSTROOT}" ]; then
+ echo "$NOT_A_LIVE_SYSTEM_BAIL_MESSAGE" >&2
+ # Improbable logging will work
+ logger -s -t nut-shutdown "$NOT_A_LIVE_SYSTEM_BAIL_MESSAGE" || true
+ exit 1
+fi
+
+# shellcheck source=/dev/null
+. "/lib/functions.sh" || {
+ # If /lib/functions.sh is missing on a live system (or we are not on a
+ # live system) bail with an error.
+ # Logging probably won't work if /lib/functions.sh is not available but try
+ echo "$MISSING_LIB_FUNCTIONS_SH_BAIL_MESSAGE" >&2
+ logger -s -t nut-shutdown "$MISSING_LIB_FUNCTIONS_SH_BAIL_MESSAGE" || true
+ exit 1
+}
+
+disable_hotplug() {
+ # If killpower is set disable hotplug until next boot
+ # (NUT_DISABLE_HOTPLUG_PATH) *should* be on non-persistent media that is
+ # available during forced shutdown
+ # If it is on persistent storage, manual intervention will be required
+ # for the NUT hotplug scripts to execute again
+ if [ ! -f "$NUT_DISABLE_HOTPLUG_PATH" ]; then
+ mkdir -p "$(dirname "$NUT_DISABLE_HOTPLUG_PATH")" 2>/dev/null || true
+ # Try to touch the file even if mkdir fails, but swallow an errors,
+ # because this is a best-effort emergency shutdown.
+ touch "$NUT_DISABLE_HOTPLUG_PATH" 2>/dev/null || true
fi
}
-do_fsd
+# If a forced shutdown has been triggered ($NUT_KILLPOWER is present)
+# and we have a server config and the config loads correctly
+if [ -f "$NUT_KILLPOWER" ] && [ -f "$NUT_SERVER_CONFIG" ] && config_load nut_server; then
+ DO_KILLPOWER="1"
+
+ # UPS will wait 'offdelay' before shutting down
+ disable_hotplug || true
+
+ stop_drivers || true
+ remount_filesystems_as_readonly || true
+
+ # Drivers are already stopped so we don't require r/w access to filesystems
+ config_foreach shutdown_actual_ups driver || true
+elif [ -f "$NUT_KILLPOWER" ]; then
+ # If we can't load config, attempt to log that fact, but shutdown anyway
+ DO_KILLPOWER="1"
+ disable_hotplug || true
+ echo "$MISSING_CONFIG_BAIL_MESSAGE" >&2 || true
+ logger -s -t nut-shutdown "$MISSING_CONFIG_BAIL_MESSAGE" || true
+
+ remount_filesystems_as_readonly || true
+fi
+
+if [ "$DO_KILLPOWER" = "1" ]; then
+ # This is it, if the forced shutdown doesn't work we just wait for power
+ # to cutoff
+ do_forced_shutdown
+else
+ # Not a forced shutdown, just do a normal poweroff (calls normal
+ # power down sequence). Do not ignore errors in this case.
+ poweroff
+fi
+
+# To be explicit to readers that this script does nothing after this
+# It's an error exit, as it should never be reached
+exit 1