#!/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. # # In recent (relevant) versions of shellcheck busybox is a valid shell type # shellcheck shell=busybox # shellcheck disable=SC2034 START=70 STOP=30 # We use the script functions and variables definitions from # /etc/rc.common that are added by USE_PROCD except with a modified # start and rc_procd. We cannot simply override USE_PROCD's # implementation of start and rc_procd as rc.common sources this # initscript before the definitions are applied by USE_PROCD # # The modified rc_procd uses "set" for procd_close_service, which # registers the service with procd, if the service for this # initscript (nut-server) is not registered with procd (active) at all, # otherwise it uses "add" which adds an instance to the registered # (active) service. # # USE_PROCD=1 # 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 -t "nut-server[$$]" "FATAL: Unable to source 'functions.sh' in 'nut-server'" 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 -t "nut-server[$$]" "FATAL: Unable to source 'nut-common.sh' in 'nut-server'" exit 1 } # shellcheck source=/dev/null . "${IPKG_INSTROOT}"/lib/functions/network.sh || { log_source_error "network.sh" "nut-server" "nut-server" exit 1 } # 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 } # shellcheck source=/dev/null . "${IPKG_INSTROOT}"/usr/share/libubox/jshn.sh || { log_source_error "jshn.sh" "nut-server" "nut-server" 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-server" "nut-server" exit 1 } # shellcheck source=net/nut/files/nut-server-service.sh.functions . "${IPKG_INSTROOT}"/lib/functions/nut/nut-server-service.sh || { log_source_error "nut-server-service.sh" "nut-server" "nut-server" exit 1 } # shellcheck source=/dev/null . "${IPKG_INSTROOT}"/lib/functions/procd.sh || { log_source_error "procd.sh" "nut-server" "nut-server" exit 1 } restore_umask_on_exit extra_command "running" "Check if service is running" extra_command "status" "Service status" extra_command "trace" "Start with syscall trace" extra_command "info" "Dump procd service info" basescript=$(readlink "$initscript") has_running_driver() { local instances instance # have_driver_instance and have_upsd_instance are 'pseudo-globals' - # they are local to the _caller_ and visible/modifiable in this # function in order to get the value back to the caller. instances="$(list_running_instances "nut-server")" [ -n "$instances" ] || return 0 set -f for instance in $instances; do if [ "$instance" = "upsd" ]; then have_upsd_instance="true" continue fi have_driver_instance="true" break done set +f } start_server_service() { procd_open_instance upsd procd_set_param respawn procd_set_param stderr 1 procd_set_param stdout 1 procd_set_param env NUT_DEBUG_SYSLOG="stderr" procd_set_param env NUT_QUIET_INIT_UPSNOTIFY=true procd_set_param env NUT_STATEPATH="$STATEPATH" procd_set_param command /usr/sbin/upsd procd_append_param command -FF procd_append_param command -u "$RUNAS" procd_close_instance } # Start upsd instance start_server_instance() { if [ -f "$NUT_KILLPOWER" ]; then return 1 fi local have_driver_instance="false" local have_upsd_instance="false" has_running_driver # upsd will crash if started with no available UPS, so only start if # we have one, and we do not already have a upsd instance if [ "$have_driver_instance" = "true" ] && [ "$have_upsd_instance" = "false" ]; then rc_procd start_server_service || return 1 fi } start_ups_service() { procd_open_instance "$ups" procd_set_param respawn procd_set_param stderr 1 procd_set_param stdout 1 procd_set_param env NUT_DEBUG_SYSLOG="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}" procd_append_param command -FF -a "$ups" procd_append_param command -u "$RUNAS" procd_close_instance } # Start a ups driver instance start_ups_driver() { local ups="$1" local requested="$2" local driver="" # If wanting a specific instance, only start that instance; this is a normal # operating event, so do not log and do not error if [ -n "$requested" ] && [ "$requested" != "$ups" ]; then return 0 fi # 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" || { usb_ups_access_error="true" log_error "Error enabling USB access for UPS '$ups'" "nut-server" "nut-server" return 1 } config_get driver "$ups" driver [ -n "$driver" ] || { log_error "Missing driver when starting UPS '$ups'" "nut-server" "nut-server" return 1 } rc_procd start_ups_service "$@" || { return 1 } return 0 } have_upsd_section() { have_upsd_section="true" } common_preconditions() { local ret # No config, or error loading config is fatal config_load nut_server || { log_config_load_error "nut_server" "nut-server" "nut-server" exit 1 } # Only find statepath and runas once per service start or reload if have_section_named "upsd" "upsd"; then STATEPATH="$(find_statepath "upsd" "nut_server")" || { log_error_exit "Failed to determine STATEPATH" "nut-server" "nut-server" } RUNAS="$(find_runas "upsd" "nut_server")" || { log_error_exit "Failed to determine RUNAS" "nut-server" "nut-server" } else # If there is no 'upsd' section in the nut_server config file, use # default settings STATEPATH=/var/run/nut RUNAS=nut fi # NUT_KILLPOWER is an external sentinel that indicates a forced shutdown if [ -f "$NUT_KILLPOWER" ]; then return 1 fi build_config ret=$? return $ret } stop_no_longer_configured_instances() { local have_driver_instance=false local have_upsd_instance=false local had_running_instance="false" local instance instances # Stop any driver instances which are no longer configured # This can occur when the configuration is changed and the service # is restarted or reloaded, as this script is not notified of what has # changed if the configuration changes; the script will simply # generate a new configuration in that event. # We can only reliably do this for instances managed by procd instances="$(list_running_instances "nut-server")" [ -n "$instances" ] || return 0 set -f # Check that all instances are associated with a configured driver or # the upsd daemon for instance in $instances; do # upsd should be stopped last, if it is stopped at all if [ "$instance" = "upsd" ]; then continue fi # Set to true if any instance was running, whether stopped here, or left # running. have_driver_instance (set in has_running_driver) lets us know # if any driver is still running. had_running_instance="true" 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 log_msg "Stopping no longer configured ups '$instance'" "nut-server" "nut-server" notice procd_kill "nut-server" "$instance" fi done set +f # Need to stop drivers before stopping upsd # The has_running_driver check 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 starting to start # instances between the stop above, and this loop. has_running_driver # 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, but avoid # signalling upsd if it is not running ('had_running_instance' != "true") if [ "$have_upsd_instance" = "true" ] && [ "$have_driver_instance" = "false" ] && [ "$had_running_instance" = "true" ]; then log_msg "Stopping upsd because no driver instances configured" "nut-server" "nut-server" "notice" signal_instance "upsd" "upsd" "stop" "TERM" "${STATEPATH}/upsd.pid" "" "nut-server" "procd_kill" "nut-server" "upsd" elif [ "$have_upsd_instance" = "false" ] && [ "$have_driver_instance" = "true" ]; then start_server_instance fi } stop_all_instances() { local instance instances instances="$(list_running_instances "nut-server")" [ -n "$instances" ] || return 0 set -f for instance in $instances; do # stop upsd last if [ "$instance" = "upsd" ]; then continue fi config_get driver "$instance" driver [ -n "$driver" ] || continue signal_instance "$instance" "/usr/libexec/nut/${driver}" "stop" "TERM" \ "${STATEPATH}/${driver}-${instance}.pid" "" "nut-server" "procd_kill" "nut-server" "$instance" done set +f # stop_server_instance signal_instance "upsd" "upsd" "stop" "TERM" "${STATEPATH}/upsd.pid" "" "nut-server" "procd_kill" "nut-server" "upsd" return 0 } 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) check_interface_up "upsd" || should_start_srv=false ;; *) should_start_srv=false ;; esac ;; reload) common_preconditions ret=$? case $ret in 0 | 2) check_interface_up "upsd" || should_stop_srv=true ;; *) should_stop_srv=true ;; esac ;; *) log_error "Unknown action '$action' in service_preconditions" "nut-server" "nut-server" ret=1 ;; esac # 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 # 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 # 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 } 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 [ -n "$requested" ] && [ "$requested" != "$ups" ]; then return 0 fi config_get driver "$ups" driver 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 # 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 [ -n "$requested" ] && [ "$requested" != "$ups" ]; then return 0 fi if [ -f "$NUT_KILLPOWER" ]; then stop_ups_driver "$ups" return 0 fi config_get driver "$ups" driver # 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 # If the driver instance (UPS) was terminated, or stopped 'naturally', # start it up again if ! procd_running "nut-server" "$ups" >/dev/null 2>&1; then start_ups_driver "$ups" "$ups" fi # If the server instance (upsd) was terminated, or stopped 'naturally', # start it up again start_server_instance return 0 } stop_service_if_no_instances() { if service_active_no_instances "nut-server"; then log_msg "nut-server active with no instances" nut-server "nut-server" notice # 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 -t "nut-server" fi } # Stop or reload active instances manage_instances() { local action="$1" case "$action" in start) config_foreach start_ups_driver driver start_server_instance ;; reload) if service_active_no_instances "nut-server"; then log_msg "nut-server active with no instances. Reloading." "nut-server" "nut-server" "info" fi config_foreach 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 } # 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 local service_active case "$action" in start) service_preconditions "$action" ret=$? case "$instance" in "") if [ "$ret" = "0" ]; then manage_instances start else return 1 fi ;; # We only start one service (upsd or one driver) from a given invocation upsd) if [ "$ret" = "0" ]; then start_server_instance fi ;; # We only start one service (upsd or one driver) from a given invocation *) if [ "$ret" = "0" ]; then config_foreach start_ups_driver driver "$instance" start_server_instance fi ;; esac # Ensure drivers no longer present in config, but which are still running # get stopped. This is required because we are not notified of what was # removed when configuration is removed. stop_no_longer_configured_instances return $ret ;; reload) service_preconditions "$action" ret=$? if [ "$ret" != "1" ]; then manage_instances reload fi if [ "$ret" = "1" ]; then return 1 fi ;; stop) case "$instance" in "") manage_instances stop ret=$? service_active_no_instances "$(basename "${basescript:-$initscript}")" service_active=$? if [ "$service_active" -eq 1 ]; then # Prevent NUT hotplug events when service is not running rm -f "$NUT_HOTPLUG_BOOT_COMPLETE_PATH" elif [ "$service_active" -eq 3 ]; then return 1 fi return $ret ;; 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 ;; 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 } allow_hotplug() { local ret="$1" [ "$ret" -eq 0 ] || return "$ret" # Allow hotplug events to (re)start drivers/upsd only if starting # nut-server service was successful touch "$NUT_HOTPLUG_BOOT_COMPLETE_PATH" # Ensure drivers/upsd started once hotplug has been allowed local usb_ups_access_error="false" config_foreach start_ups_driver driver if [ "$usb_ups_access_error" = "false" ]; then start_server_instance ret=$? else ret=1 fi # Re-disable hotplug if starting drivers and/or upsd failed after # enabling hotplug. if [ "$ret" -ne 0 ]; then rm -f "$NUT_HOTPLUG_BOOT_COMPLETE_PATH" fi return "$ret" } # Start NUT drivers and upsd (NUT server) start_service() { local ret manage_service start "$@" ret=$? # Only change allowing hotplug events for the # start all instances case if [ -z "$1" ]; then allow_hotplug "$ret" fi } # Reload NUT drivers and upsd (NUT server) reload_service() { local ret manage_service reload "$@" ret=$? # Only change allowing hotplug events for the # reload all instances case if [ -z "$1" ]; then # Allow hotplug events to (re)start drivers/upsd when reloading # (successfully) after a failed start allow_hotplug "$ret" fi } # Stop NUT drivers and upsd (NUT server) stop_service() { manage_service stop "$@" } service_triggers() { # 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 if ! check_unsigned_int "$interface_reload_delay" || [ -z "$interface_reload_delay" ]; then log_msg "interface_reload_delay must be an unsigned integer" nut-server nut-server warn interface_reload_delay="$DEFAULT_PROCD_INTERFACE_RELOAD_DELAY" fi add_interface_triggers "upsd" "$interface_reload_delay" "nut-server" "upsd" || { log_error "Failed to add interface triggers" nut-server nut-server } procd_add_reload_trigger "nut_server" } rc_procd() { local method service_active service_active_no_instances "$(basename "${basescript:-$initscript}")" service_active=$? # Always use set if nut-server service is not active, even if this # function is executed from reload or restart. In the default # implementation from USE_PROCD=1, rc_procd is only called from # start, so set is only used on a start action. In addition, the # default rc_procd only uses add when called with a second parameter if [ "$service_active" -eq 1 ]; then log_msg "$(basename "${basescript:-$initscript}") was not active, so start it" "nut-server" "nut-server" "debug" method="set" elif [ "$service_active" -eq 3 ]; then # log error message was already issued in service_active_no_instances return 1 elif [ "$service_active" -eq 0 ] || [ "$service_active" -eq 2 ]; then method="add" else log_error "Unexpected return value from 'service_active_no_instances'" "nut-server" "nut-server" return 1 fi procd_open_service "$(basename "${basescript:-$initscript}")" "$initscript" "$@" procd_close_service "$method" return 0 } # Define start in a way that avoids nesting rc_procd start() { # Ensure start actions occur one at a time procd_lock # Ensure service is started even if no instances are started if rc_procd :; then start_service "$@" fi } boot() { start } trace() { TRACE_SYSCALLS=1 start "$@" } info() { json_init json_add_string name "$(basename "${basescript:-$initscript}")" json_add_boolean verbose "1" _procd_ubus_call list } stop() { procd_lock stop_service "$@" procd_kill "$(basename "${basescript:-$initscript}")" "$1" if eval "type service_stopped" 2>/dev/null >/dev/null; then service_stopped fi } reload() { if eval "type reload_service" 2>/dev/null >/dev/null; then procd_lock reload_service "$@" else start fi } running() { service_running "$@" } status() { if eval "type status_service" 2>/dev/null >/dev/null; then status_service "$@" else _procd_status "$(basename "${basescript:-$initscript}")" "$1" fi }