Files
packages/net/nut/files/nut-server.init
T
Daniel F. Dickinson 2537757a3b nut: improve documentation
Include more inline comments to document the nut-server initscript and
usb hotplug.

Also trim an extraneous space in a comment.

Signed-off-by: Daniel F. Dickinson <dfdpublic@wildtechgarden.ca>
2026-09-10 10:22:36 +02:00

770 lines
21 KiB
Bash

#!/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
}