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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
|
#!/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
# shellcheck disable=SC2034
STATEPATH=""
# shellcheck disable=SC2034
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
}
# Store path for NUT working data in the global variable STATEPATH
# shellcheck disable=SC2329
find_statepath() {
local section="$1"
local package="$2"
local ret statepath
# Subshell so as not to interfere with main config_load
(
config_load "$package" || {
log_config_load_error "$package" "nut-service.sh" "nut-service"
exit 1
}
config_get statepath "$section" statepath "$NUT_BASE_STATEPATH"
ret=$?
if [ -n "$statepath" ]; then
printf "%s" "$statepath"
elif [ "$ret" != "0" ]; then
exit $ret
else
exit 1
fi
) || return $?
}
# Store user under which to run daemon processes
# shellcheck disable=SC2329
find_runas() {
local section="$1"
local package="$2"
local fallback_runas="$3"
local runas ret
# Subshell so as not to interfere with main config_load
(
config_load "$package" || {
log_config_load_error "$package" "nut-service.sh" "nut-service"
exit 1
}
config_get runas "$section" runas "${fallback_runas:-nut}"
ret=$?
if [ -z "$runas" ]; then
if [ "$ret" != "0" ]; then
exit $ret
else
exit 1
fi
fi
if [ -n "$(id -un "$runas")" ]; then
printf "%s" "$runas"
else
# We omit the output on stderr to avoid potential mixing with the intended printf output on stdout,
# when called via command substitution, while maintaining log output to syslog
log_error "User '$runas' specified for 'runas' does not exist." nut-service.sh nut-service 2>/dev/null
exit 1
fi
) || return $?
}
# 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
add_interface_triggers() {
local section="$1"
local interface_reload_delay="${2:-$DEFAULT_PROCD_INTERFACE_RELOAD_DELAY}"
local initscript="$3"
if [ $# -ge 3 ]; then
shift 3
fi
local interfaces interface trigger_failed
config_get interfaces "$section" triggerlist
if [ -n "$interfaces" ] && [ "$interfaces" != "all" ]; 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
# fifth and following parameters of procd_add_interface_trigger are passed through to
# the <script>, so the trigger action becomes e.g. /etc/init.d/<script> restart <variadic_vars>.
procd_add_interface_trigger "interface.*.up" "$interface" "/etc/init.d/$initscript" "restart" "$@" || trigger_failed="true"
done
set +f
if [ "$trigger_failed" = "true" ]; then
return 1
fi
elif [ "$interfaces" = "all" ]; then
# Add a restart trigger on any interface's up status changing
procd_add_raw_trigger "interface.*.up" "$interface_reload_delay" "/etc/init.d/$initscript" "restart" "$@" || return 1
else
return 0 # don't react to interface status
fi
return 0
}
# availability of network_is_up depends on /lib/functions/network.sh having
# been sourced
check_interface_up() {
local section="$1"
local interfaces interface have_up_interface
config_get interfaces "$section" triggerlist
if [ -n "$interfaces" ] && [ "$interfaces" != "all" ]; 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
elif [ "$interfaces" = "all" ]; then
# 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
else
return 0 # don't depend on interface status
fi
return 0
}
|