1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
|
#!/bin/sh
#
# 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
# 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 -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
}
# 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 -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 "$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
}
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 -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 -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
}
# 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 -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
|