]> git.proxmox.com Git - mirror_zfs.git/blob - cmd/zed/zed.d/zed-functions.sh
Enable shellcheck to run for select scripts
[mirror_zfs.git] / cmd / zed / zed.d / zed-functions.sh
1 #!/bin/sh
2 # shellcheck disable=SC2039
3 # zed-functions.sh
4 #
5 # ZED helper functions for use in ZEDLETs
6
7
8 # Variable Defaults
9 #
10 : "${ZED_LOCKDIR:="/var/lock"}"
11 : "${ZED_NOTIFY_INTERVAL_SECS:=3600}"
12 : "${ZED_NOTIFY_VERBOSE:=0}"
13 : "${ZED_RUNDIR:="/var/run"}"
14 : "${ZED_SYSLOG_PRIORITY:="daemon.notice"}"
15 : "${ZED_SYSLOG_TAG:="zed"}"
16
17 ZED_FLOCK_FD=8
18
19
20 # zed_check_cmd (cmd, ...)
21 #
22 # For each argument given, search PATH for the executable command [cmd].
23 # Log a message if [cmd] is not found.
24 #
25 # Arguments
26 # cmd: name of executable command for which to search
27 #
28 # Return
29 # 0 if all commands are found in PATH and are executable
30 # n for a count of the command executables that are not found
31 #
32 zed_check_cmd()
33 {
34 local cmd
35 local rv=0
36
37 for cmd; do
38 if ! command -v "${cmd}" >/dev/null 2>&1; then
39 zed_log_err "\"${cmd}\" not installed"
40 rv=$((rv + 1))
41 fi
42 done
43 return "${rv}"
44 }
45
46
47 # zed_log_msg (msg, ...)
48 #
49 # Write all argument strings to the system log.
50 #
51 # Globals
52 # ZED_SYSLOG_PRIORITY
53 # ZED_SYSLOG_TAG
54 #
55 # Return
56 # nothing
57 #
58 zed_log_msg()
59 {
60 logger -p "${ZED_SYSLOG_PRIORITY}" -t "${ZED_SYSLOG_TAG}" -- "$@"
61 }
62
63
64 # zed_log_err (msg, ...)
65 #
66 # Write an error message to the system log. This message will contain the
67 # script name, EID, and all argument strings.
68 #
69 # Globals
70 # ZED_SYSLOG_PRIORITY
71 # ZED_SYSLOG_TAG
72 # ZEVENT_EID
73 #
74 # Return
75 # nothing
76 #
77 zed_log_err()
78 {
79 logger -p "${ZED_SYSLOG_PRIORITY}" -t "${ZED_SYSLOG_TAG}" -- "error:" \
80 "$(basename -- "$0"):""${ZEVENT_EID:+" eid=${ZEVENT_EID}:"}" "$@"
81 }
82
83
84 # zed_lock (lockfile, [fd])
85 #
86 # Obtain an exclusive (write) lock on [lockfile]. If the lock cannot be
87 # immediately acquired, wait until it becomes available.
88 #
89 # Every zed_lock() must be paired with a corresponding zed_unlock().
90 #
91 # By default, flock-style locks associate the lockfile with file descriptor 8.
92 # The bash manpage warns that file descriptors >9 should be used with care as
93 # they may conflict with file descriptors used internally by the shell. File
94 # descriptor 9 is reserved for zed_rate_limit(). If concurrent locks are held
95 # within the same process, they must use different file descriptors (preferably
96 # decrementing from 8); otherwise, obtaining a new lock with a given file
97 # descriptor will release the previous lock associated with that descriptor.
98 #
99 # Arguments
100 # lockfile: pathname of the lock file; the lock will be stored in
101 # ZED_LOCKDIR unless the pathname contains a "/".
102 # fd: integer for the file descriptor used by flock (OPTIONAL unless holding
103 # concurrent locks)
104 #
105 # Globals
106 # ZED_FLOCK_FD
107 # ZED_LOCKDIR
108 #
109 # Return
110 # nothing
111 #
112 zed_lock()
113 {
114 local lockfile="$1"
115 local fd="${2:-${ZED_FLOCK_FD}}"
116 local umask_bak
117 local err
118
119 [ -n "${lockfile}" ] || return
120 if ! expr "${lockfile}" : '.*/' >/dev/null 2>&1; then
121 lockfile="${ZED_LOCKDIR}/${lockfile}"
122 fi
123
124 umask_bak="$(umask)"
125 umask 077
126
127 # Obtain a lock on the file bound to the given file descriptor.
128 #
129 eval "exec ${fd}> '${lockfile}'"
130 err="$(flock --exclusive "${fd}" 2>&1)"
131 # shellcheck disable=SC2181
132 if [ $? -ne 0 ]; then
133 zed_log_err "failed to lock \"${lockfile}\": ${err}"
134 fi
135
136 umask "${umask_bak}"
137 }
138
139
140 # zed_unlock (lockfile, [fd])
141 #
142 # Release the lock on [lockfile].
143 #
144 # Arguments
145 # lockfile: pathname of the lock file
146 # fd: integer for the file descriptor used by flock (must match the file
147 # descriptor passed to the zed_lock function call)
148 #
149 # Globals
150 # ZED_FLOCK_FD
151 # ZED_LOCKDIR
152 #
153 # Return
154 # nothing
155 #
156 zed_unlock()
157 {
158 local lockfile="$1"
159 local fd="${2:-${ZED_FLOCK_FD}}"
160 local err
161
162 [ -n "${lockfile}" ] || return
163 if ! expr "${lockfile}" : '.*/' >/dev/null 2>&1; then
164 lockfile="${ZED_LOCKDIR}/${lockfile}"
165 fi
166
167 # Release the lock and close the file descriptor.
168 err="$(flock --unlock "${fd}" 2>&1)"
169 # shellcheck disable=SC2181
170 if [ $? -ne 0 ]; then
171 zed_log_err "failed to unlock \"${lockfile}\": ${err}"
172 fi
173 eval "exec ${fd}>&-"
174 }
175
176
177 # zed_notify (subject, pathname)
178 #
179 # Send a notification via all available methods.
180 #
181 # Arguments
182 # subject: notification subject
183 # pathname: pathname containing the notification message (OPTIONAL)
184 #
185 # Return
186 # 0: notification succeeded via at least one method
187 # 1: notification failed
188 # 2: no notification methods configured
189 #
190 zed_notify()
191 {
192 local subject="$1"
193 local pathname="$2"
194 local num_success=0
195 local num_failure=0
196
197 zed_notify_email "${subject}" "${pathname}"; rv=$?
198 [ "${rv}" -eq 0 ] && num_success=$((num_success + 1))
199 [ "${rv}" -eq 1 ] && num_failure=$((num_failure + 1))
200
201 zed_notify_pushbullet "${subject}" "${pathname}"; rv=$?
202 [ "${rv}" -eq 0 ] && num_success=$((num_success + 1))
203 [ "${rv}" -eq 1 ] && num_failure=$((num_failure + 1))
204
205 [ "${num_success}" -gt 0 ] && return 0
206 [ "${num_failure}" -gt 0 ] && return 1
207 return 2
208 }
209
210
211 # zed_notify_email (subject, pathname)
212 #
213 # Send a notification via email to the address specified by ZED_EMAIL_ADDR.
214 #
215 # Requires the mail executable to be installed in the standard PATH, or
216 # ZED_EMAIL_PROG to be defined with the pathname of an executable capable of
217 # reading a message body from stdin.
218 #
219 # Command-line options to the mail executable can be specified in
220 # ZED_EMAIL_OPTS. This undergoes the following keyword substitutions:
221 # - @ADDRESS@ is replaced with the space-delimited recipient email address(es)
222 # - @SUBJECT@ is replaced with the notification subject
223 #
224 # Arguments
225 # subject: notification subject
226 # pathname: pathname containing the notification message (OPTIONAL)
227 #
228 # Globals
229 # ZED_EMAIL_PROG
230 # ZED_EMAIL_OPTS
231 # ZED_EMAIL_ADDR
232 #
233 # Return
234 # 0: notification sent
235 # 1: notification failed
236 # 2: not configured
237 #
238 zed_notify_email()
239 {
240 local subject="$1"
241 local pathname="${2:-"/dev/null"}"
242
243 : "${ZED_EMAIL_PROG:="mail"}"
244 : "${ZED_EMAIL_OPTS:="-s '@SUBJECT@' @ADDRESS@"}"
245
246 # For backward compatibility with ZED_EMAIL.
247 if [ -n "${ZED_EMAIL}" ] && [ -z "${ZED_EMAIL_ADDR}" ]; then
248 ZED_EMAIL_ADDR="${ZED_EMAIL}"
249 fi
250 [ -n "${ZED_EMAIL_ADDR}" ] || return 2
251
252 zed_check_cmd "${ZED_EMAIL_PROG}" || return 1
253
254 [ -n "${subject}" ] || return 1
255 if [ ! -r "${pathname}" ]; then
256 zed_log_err \
257 "$(basename "${ZED_EMAIL_PROG}") cannot read \"${pathname}\""
258 return 1
259 fi
260
261 ZED_EMAIL_OPTS="$(echo "${ZED_EMAIL_OPTS}" \
262 | sed -e "s/@ADDRESS@/${ZED_EMAIL_ADDR}/g" \
263 -e "s/@SUBJECT@/${subject}/g")"
264
265 # shellcheck disable=SC2086
266 eval "${ZED_EMAIL_PROG}" ${ZED_EMAIL_OPTS} < "${pathname}" >/dev/null 2>&1
267 rv=$?
268 if [ "${rv}" -ne 0 ]; then
269 zed_log_err "$(basename "${ZED_EMAIL_PROG}") exit=${rv}"
270 return 1
271 fi
272 return 0
273 }
274
275
276 # zed_notify_pushbullet (subject, pathname)
277 #
278 # Send a notification via Pushbullet <https://www.pushbullet.com/>.
279 # The access token (ZED_PUSHBULLET_ACCESS_TOKEN) identifies this client to the
280 # Pushbullet server. The optional channel tag (ZED_PUSHBULLET_CHANNEL_TAG) is
281 # for pushing to notification feeds that can be subscribed to; if a channel is
282 # not defined, push notifications will instead be sent to all devices
283 # associated with the account specified by the access token.
284 #
285 # Requires awk, curl, and sed executables to be installed in the standard PATH.
286 #
287 # References
288 # https://docs.pushbullet.com/
289 # https://www.pushbullet.com/security
290 #
291 # Arguments
292 # subject: notification subject
293 # pathname: pathname containing the notification message (OPTIONAL)
294 #
295 # Globals
296 # ZED_PUSHBULLET_ACCESS_TOKEN
297 # ZED_PUSHBULLET_CHANNEL_TAG
298 #
299 # Return
300 # 0: notification sent
301 # 1: notification failed
302 # 2: not configured
303 #
304 zed_notify_pushbullet()
305 {
306 local subject="$1"
307 local pathname="${2:-"/dev/null"}"
308 local msg_body
309 local msg_tag
310 local msg_json
311 local msg_out
312 local msg_err
313 local url="https://api.pushbullet.com/v2/pushes"
314
315 [ -n "${ZED_PUSHBULLET_ACCESS_TOKEN}" ] || return 2
316
317 [ -n "${subject}" ] || return 1
318 if [ ! -r "${pathname}" ]; then
319 zed_log_err "pushbullet cannot read \"${pathname}\""
320 return 1
321 fi
322
323 zed_check_cmd "awk" "curl" "sed" || return 1
324
325 # Escape the following characters in the message body for JSON:
326 # newline, backslash, double quote, horizontal tab, vertical tab,
327 # and carriage return.
328 #
329 msg_body="$(awk '{ ORS="\\n" } { gsub(/\\/, "\\\\"); gsub(/"/, "\\\"");
330 gsub(/\t/, "\\t"); gsub(/\f/, "\\f"); gsub(/\r/, "\\r"); print }' \
331 "${pathname}")"
332
333 # Push to a channel if one is configured.
334 #
335 [ -n "${ZED_PUSHBULLET_CHANNEL_TAG}" ] && msg_tag="$(printf \
336 '"channel_tag": "%s", ' "${ZED_PUSHBULLET_CHANNEL_TAG}")"
337
338 # Construct the JSON message for pushing a note.
339 #
340 msg_json="$(printf '{%s"type": "note", "title": "%s", "body": "%s"}' \
341 "${msg_tag}" "${subject}" "${msg_body}")"
342
343 # Send the POST request and check for errors.
344 #
345 msg_out="$(curl -u "${ZED_PUSHBULLET_ACCESS_TOKEN}:" -X POST "${url}" \
346 --header "Content-Type: application/json" --data-binary "${msg_json}" \
347 2>/dev/null)"; rv=$?
348 if [ "${rv}" -ne 0 ]; then
349 zed_log_err "curl exit=${rv}"
350 return 1
351 fi
352 msg_err="$(echo "${msg_out}" \
353 | sed -n -e 's/.*"error" *:.*"message" *: *"\([^"]*\)".*/\1/p')"
354 if [ -n "${msg_err}" ]; then
355 zed_log_err "pushbullet \"${msg_err}"\"
356 return 1
357 fi
358 return 0
359 }
360
361
362 # zed_rate_limit (tag, [interval])
363 #
364 # Check whether an event of a given type [tag] has already occurred within the
365 # last [interval] seconds.
366 #
367 # This function obtains a lock on the statefile using file descriptor 9.
368 #
369 # Arguments
370 # tag: arbitrary string for grouping related events to rate-limit
371 # interval: time interval in seconds (OPTIONAL)
372 #
373 # Globals
374 # ZED_NOTIFY_INTERVAL_SECS
375 # ZED_RUNDIR
376 #
377 # Return
378 # 0 if the event should be processed
379 # 1 if the event should be dropped
380 #
381 # State File Format
382 # time;tag
383 #
384 zed_rate_limit()
385 {
386 local tag="$1"
387 local interval="${2:-${ZED_NOTIFY_INTERVAL_SECS}}"
388 local lockfile="zed.zedlet.state.lock"
389 local lockfile_fd=9
390 local statefile="${ZED_RUNDIR}/zed.zedlet.state"
391 local time_now
392 local time_prev
393 local umask_bak
394 local rv=0
395
396 [ -n "${tag}" ] || return 0
397
398 zed_lock "${lockfile}" "${lockfile_fd}"
399 time_now="$(date +%s)"
400 time_prev="$(egrep "^[0-9]+;${tag}\$" "${statefile}" 2>/dev/null \
401 | tail -1 | cut -d\; -f1)"
402
403 if [ -n "${time_prev}" ] \
404 && [ "$((time_now - time_prev))" -lt "${interval}" ]; then
405 rv=1
406 else
407 umask_bak="$(umask)"
408 umask 077
409 egrep -v "^[0-9]+;${tag}\$" "${statefile}" 2>/dev/null \
410 > "${statefile}.$$"
411 echo "${time_now};${tag}" >> "${statefile}.$$"
412 mv -f "${statefile}.$$" "${statefile}"
413 umask "${umask_bak}"
414 fi
415
416 zed_unlock "${lockfile}" "${lockfile_fd}"
417 return "${rv}"
418 }
419
420
421 # zed_guid_to_pool (guid)
422 #
423 # Convert a pool GUID into its pool name (like "tank")
424 # Arguments
425 # guid: pool GUID (decimal or hex)
426 #
427 # Return
428 # Pool name
429 #
430 zed_guid_to_pool()
431 {
432 if [ -z "$1" ] ; then
433 return
434 fi
435
436 guid=$(printf "%llu" "$1")
437 if [ ! -z "$guid" ] ; then
438 $ZPOOL get -H -ovalue,name guid | awk '$1=='"$guid"' {print $2}'
439 fi
440 }