1 #!/bin/sh
2 # shellcheck disable=SC2154,SC3043
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 zed_log_msg "error: ${0##*/}:""${ZEVENT_EID:+" eid=${ZEVENT_EID}:"}" "$@"
80 }
81
82
83 # zed_lock (lockfile, [fd])
84 #
85 # Obtain an exclusive (write) lock on [lockfile]. If the lock cannot be
86 # immediately acquired, wait until it becomes available.
87 #
88 # Every zed_lock() must be paired with a corresponding zed_unlock().
89 #
90 # By default, flock-style locks associate the lockfile with file descriptor 8.
91 # The bash manpage warns that file descriptors >9 should be used with care as
92 # they may conflict with file descriptors used internally by the shell. File
93 # descriptor 9 is reserved for zed_rate_limit(). If concurrent locks are held
94 # within the same process, they must use different file descriptors (preferably
95 # decrementing from 8); otherwise, obtaining a new lock with a given file
96 # descriptor will release the previous lock associated with that descriptor.
97 #
98 # Arguments
99 # lockfile: pathname of the lock file; the lock will be stored in
100 # ZED_LOCKDIR unless the pathname contains a "/".
101 # fd: integer for the file descriptor used by flock (OPTIONAL unless holding
102 # concurrent locks)
103 #
104 # Globals
105 # ZED_FLOCK_FD
106 # ZED_LOCKDIR
107 #
108 # Return
109 # nothing
110 #
111 zed_lock()
112 {
113 local lockfile="$1"
114 local fd="${2:-${ZED_FLOCK_FD}}"
115 local umask_bak
116 local err
117
118 [ -n "${lockfile}" ] || return
119 if ! expr "${lockfile}" : '.*/' >/dev/null 2>&1; then
120 lockfile="${ZED_LOCKDIR}/${lockfile}"
121 fi
122
123 umask_bak="$(umask)"
124 umask 077
125
126 # Obtain a lock on the file bound to the given file descriptor.
127 #
128 eval "exec ${fd}>> '${lockfile}'"
129 if ! err="$(flock --exclusive "${fd}" 2>&1)"; then
130 zed_log_err "failed to lock \"${lockfile}\": ${err}"
131 fi
132
133 umask "${umask_bak}"
134 }
135
136
137 # zed_unlock (lockfile, [fd])
138 #
139 # Release the lock on [lockfile].
140 #
141 # Arguments
142 # lockfile: pathname of the lock file
143 # fd: integer for the file descriptor used by flock (must match the file
144 # descriptor passed to the zed_lock function call)
145 #
146 # Globals
147 # ZED_FLOCK_FD
148 # ZED_LOCKDIR
149 #
150 # Return
151 # nothing
152 #
153 zed_unlock()
154 {
155 local lockfile="$1"
156 local fd="${2:-${ZED_FLOCK_FD}}"
157 local err
158
159 [ -n "${lockfile}" ] || return
160 if ! expr "${lockfile}" : '.*/' >/dev/null 2>&1; then
161 lockfile="${ZED_LOCKDIR}/${lockfile}"
162 fi
163
164 # Release the lock and close the file descriptor.
165 if ! err="$(flock --unlock "${fd}" 2>&1)"; then
166 zed_log_err "failed to unlock \"${lockfile}\": ${err}"
167 fi
168 eval "exec ${fd}>&-"
169 }
170
171
172 # zed_notify (subject, pathname)
173 #
174 # Send a notification via all available methods.
175 #
176 # Arguments
177 # subject: notification subject
178 # pathname: pathname containing the notification message (OPTIONAL)
179 #
180 # Return
181 # 0: notification succeeded via at least one method
182 # 1: notification failed
183 # 2: no notification methods configured
184 #
185 zed_notify()
186 {
187 local subject="$1"
188 local pathname="$2"
189 local num_success=0
190 local num_failure=0
191
192 zed_notify_email "${subject}" "${pathname}"; rv=$?
193 [ "${rv}" -eq 0 ] && num_success=$((num_success + 1))
194 [ "${rv}" -eq 1 ] && num_failure=$((num_failure + 1))
195
196 zed_notify_pushbullet "${subject}" "${pathname}"; rv=$?
197 [ "${rv}" -eq 0 ] && num_success=$((num_success + 1))
198 [ "${rv}" -eq 1 ] && num_failure=$((num_failure + 1))
199
200 zed_notify_slack_webhook "${subject}" "${pathname}"; rv=$?
201 [ "${rv}" -eq 0 ] && num_success=$((num_success + 1))
202 [ "${rv}" -eq 1 ] && num_failure=$((num_failure + 1))
203
204 zed_notify_pushover "${subject}" "${pathname}"; rv=$?
205 [ "${rv}" -eq 0 ] && num_success=$((num_success + 1))
206 [ "${rv}" -eq 1 ] && num_failure=$((num_failure + 1))
207
208 [ "${num_success}" -gt 0 ] && return 0
209 [ "${num_failure}" -gt 0 ] && return 1
210 return 2
211 }
212
213
214 # zed_notify_email (subject, pathname)
215 #
216 # Send a notification via email to the address specified by ZED_EMAIL_ADDR.
217 #
218 # Requires the mail executable to be installed in the standard PATH, or
219 # ZED_EMAIL_PROG to be defined with the pathname of an executable capable of
220 # reading a message body from stdin.
221 #
222 # Command-line options to the mail executable can be specified in
223 # ZED_EMAIL_OPTS. This undergoes the following keyword substitutions:
224 # - @ADDRESS@ is replaced with the space-delimited recipient email address(es)
225 # - @SUBJECT@ is replaced with the notification subject
226 # If @SUBJECT@ was omited here, a "Subject: ..." header will be added to notification
227 #
228 #
229 # Arguments
230 # subject: notification subject
231 # pathname: pathname containing the notification message (OPTIONAL)
232 #
233 # Globals
234 # ZED_EMAIL_PROG
235 # ZED_EMAIL_OPTS
236 # ZED_EMAIL_ADDR
237 #
238 # Return
239 # 0: notification sent
240 # 1: notification failed
241 # 2: not configured
242 #
243 zed_notify_email()
244 {
245 local subject="${1:-"ZED notification"}"
246 local pathname="${2:-"/dev/null"}"
247
248 : "${ZED_EMAIL_PROG:="mail"}"
249 : "${ZED_EMAIL_OPTS:="-s '@SUBJECT@' @ADDRESS@"}"
250
251 # For backward compatibility with ZED_EMAIL.
252 if [ -n "${ZED_EMAIL}" ] && [ -z "${ZED_EMAIL_ADDR}" ]; then
253 ZED_EMAIL_ADDR="${ZED_EMAIL}"
254 fi
255 [ -n "${ZED_EMAIL_ADDR}" ] || return 2
256
257 zed_check_cmd "${ZED_EMAIL_PROG}" || return 1
258
259 [ -n "${subject}" ] || return 1
260 if [ ! -r "${pathname}" ]; then
261 zed_log_err \
262 "${ZED_EMAIL_PROG##*/} cannot read \"${pathname}\""
263 return 1
264 fi
265
266 # construct cmdline options
267 ZED_EMAIL_OPTS_PARSED="$(echo "${ZED_EMAIL_OPTS}" \
268 | sed -e "s/@ADDRESS@/${ZED_EMAIL_ADDR}/g" \
269 -e "s/@SUBJECT@/${subject}/g")"
270
271 # pipe message to email prog
272 # shellcheck disable=SC2086,SC2248
273 {
274 # no subject passed as option?
275 if [ "${ZED_EMAIL_OPTS%@SUBJECT@*}" = "${ZED_EMAIL_OPTS}" ] ; then
276 # inject subject header
277 printf "Subject: %s\n" "${subject}"
278 fi
279 # output message
280 cat "${pathname}"
281 } |
282 eval ${ZED_EMAIL_PROG} ${ZED_EMAIL_OPTS_PARSED} >/dev/null 2>&1
283 rv=$?
284 if [ "${rv}" -ne 0 ]; then
285 zed_log_err "${ZED_EMAIL_PROG##*/} exit=${rv}"
286 return 1
287 fi
288 return 0
289 }
290
291
292 # zed_notify_pushbullet (subject, pathname)
293 #
294 # Send a notification via Pushbullet <https://www.pushbullet.com/>.
295 # The access token (ZED_PUSHBULLET_ACCESS_TOKEN) identifies this client to the
296 # Pushbullet server. The optional channel tag (ZED_PUSHBULLET_CHANNEL_TAG) is
297 # for pushing to notification feeds that can be subscribed to; if a channel is
298 # not defined, push notifications will instead be sent to all devices
299 # associated with the account specified by the access token.
300 #
301 # Requires awk, curl, and sed executables to be installed in the standard PATH.
302 #
303 # References
304 # https://docs.pushbullet.com/
305 # https://www.pushbullet.com/security
306 #
307 # Arguments
308 # subject: notification subject
309 # pathname: pathname containing the notification message (OPTIONAL)
310 #
311 # Globals
312 # ZED_PUSHBULLET_ACCESS_TOKEN
313 # ZED_PUSHBULLET_CHANNEL_TAG
314 #
315 # Return
316 # 0: notification sent
317 # 1: notification failed
318 # 2: not configured
319 #
320 zed_notify_pushbullet()
321 {
322 local subject="$1"
323 local pathname="${2:-"/dev/null"}"
324 local msg_body
325 local msg_tag
326 local msg_json
327 local msg_out
328 local msg_err
329 local url="https://api.pushbullet.com/v2/pushes"
330
331 [ -n "${ZED_PUSHBULLET_ACCESS_TOKEN}" ] || return 2
332
333 [ -n "${subject}" ] || return 1
334 if [ ! -r "${pathname}" ]; then
335 zed_log_err "pushbullet cannot read \"${pathname}\""
336 return 1
337 fi
338
339 zed_check_cmd "awk" "curl" "sed" || return 1
340
341 # Escape the following characters in the message body for JSON:
342 # newline, backslash, double quote, horizontal tab, vertical tab,
343 # and carriage return.
344 #
345 msg_body="$(awk '{ ORS="\\n" } { gsub(/\\/, "\\\\"); gsub(/"/, "\\\"");
346 gsub(/\t/, "\\t"); gsub(/\f/, "\\f"); gsub(/\r/, "\\r"); print }' \
347 "${pathname}")"
348
349 # Push to a channel if one is configured.
350 #
351 [ -n "${ZED_PUSHBULLET_CHANNEL_TAG}" ] && msg_tag="$(printf \
352 '"channel_tag": "%s", ' "${ZED_PUSHBULLET_CHANNEL_TAG}")"
353
354 # Construct the JSON message for pushing a note.
355 #
356 msg_json="$(printf '{%s"type": "note", "title": "%s", "body": "%s"}' \
357 "${msg_tag}" "${subject}" "${msg_body}")"
358
359 # Send the POST request and check for errors.
360 #
361 msg_out="$(curl -u "${ZED_PUSHBULLET_ACCESS_TOKEN}:" -X POST "${url}" \
362 --header "Content-Type: application/json" --data-binary "${msg_json}" \
363 2>/dev/null)"; rv=$?
364 if [ "${rv}" -ne 0 ]; then
365 zed_log_err "curl exit=${rv}"
366 return 1
367 fi
368 msg_err="$(echo "${msg_out}" \
369 | sed -n -e 's/.*"error" *:.*"message" *: *"\([^"]*\)".*/\1/p')"
370 if [ -n "${msg_err}" ]; then
371 zed_log_err "pushbullet \"${msg_err}"\"
372 return 1
373 fi
374 return 0
375 }
376
377
378 # zed_notify_slack_webhook (subject, pathname)
379 #
380 # Notification via Slack Webhook <https://api.slack.com/incoming-webhooks>.
381 # The Webhook URL (ZED_SLACK_WEBHOOK_URL) identifies this client to the
382 # Slack channel.
383 #
384 # Requires awk, curl, and sed executables to be installed in the standard PATH.
385 #
386 # References
387 # https://api.slack.com/incoming-webhooks
388 #
389 # Arguments
390 # subject: notification subject
391 # pathname: pathname containing the notification message (OPTIONAL)
392 #
393 # Globals
394 # ZED_SLACK_WEBHOOK_URL
395 #
396 # Return
397 # 0: notification sent
398 # 1: notification failed
399 # 2: not configured
400 #
401 zed_notify_slack_webhook()
402 {
403 [ -n "${ZED_SLACK_WEBHOOK_URL}" ] || return 2
404
405 local subject="$1"
406 local pathname="${2:-"/dev/null"}"
407 local msg_body
408 local msg_tag
409 local msg_json
410 local msg_out
411 local msg_err
412 local url="${ZED_SLACK_WEBHOOK_URL}"
413
414 [ -n "${subject}" ] || return 1
415 if [ ! -r "${pathname}" ]; then
416 zed_log_err "slack webhook cannot read \"${pathname}\""
417 return 1
418 fi
419
420 zed_check_cmd "awk" "curl" "sed" || return 1
421
422 # Escape the following characters in the message body for JSON:
423 # newline, backslash, double quote, horizontal tab, vertical tab,
424 # and carriage return.
425 #
426 msg_body="$(awk '{ ORS="\\n" } { gsub(/\\/, "\\\\"); gsub(/"/, "\\\"");
427 gsub(/\t/, "\\t"); gsub(/\f/, "\\f"); gsub(/\r/, "\\r"); print }' \
428 "${pathname}")"
429
430 # Construct the JSON message for posting.
431 #
432 msg_json="$(printf '{"text": "*%s*\\n%s"}' "${subject}" "${msg_body}" )"
433
434 # Send the POST request and check for errors.
435 #
436 msg_out="$(curl -X POST "${url}" \
437 --header "Content-Type: application/json" --data-binary "${msg_json}" \
438 2>/dev/null)"; rv=$?
439 if [ "${rv}" -ne 0 ]; then
440 zed_log_err "curl exit=${rv}"
441 return 1
442 fi
443 msg_err="$(echo "${msg_out}" \
444 | sed -n -e 's/.*"error" *:.*"message" *: *"\([^"]*\)".*/\1/p')"
445 if [ -n "${msg_err}" ]; then
446 zed_log_err "slack webhook \"${msg_err}"\"
447 return 1
448 fi
449 return 0
450 }
451
452 # zed_notify_pushover (subject, pathname)
453 #
454 # Send a notification via Pushover <https://pushover.net/>.
455 # The access token (ZED_PUSHOVER_TOKEN) identifies this client to the
456 # Pushover server. The user token (ZED_PUSHOVER_USER) defines the user or
457 # group to which the notification will be sent.
458 #
459 # Requires curl and sed executables to be installed in the standard PATH.
460 #
461 # References
462 # https://pushover.net/api
463 #
464 # Arguments
465 # subject: notification subject
466 # pathname: pathname containing the notification message (OPTIONAL)
467 #
468 # Globals
469 # ZED_PUSHOVER_TOKEN
470 # ZED_PUSHOVER_USER
471 #
472 # Return
473 # 0: notification sent
474 # 1: notification failed
475 # 2: not configured
476 #
477 zed_notify_pushover()
478 {
479 local subject="$1"
480 local pathname="${2:-"/dev/null"}"
481 local msg_body
482 local msg_out
483 local msg_err
484 local url="https://api.pushover.net/1/messages.json"
485
486 [ -n "${ZED_PUSHOVER_TOKEN}" ] && [ -n "${ZED_PUSHOVER_USER}" ] || return 2
487
488 if [ ! -r "${pathname}" ]; then
489 zed_log_err "pushover cannot read \"${pathname}\""
490 return 1
491 fi
492
493 zed_check_cmd "curl" "sed" || return 1
494
495 # Read the message body in.
496 #
497 msg_body="$(cat "${pathname}")"
498
499 if [ -z "${msg_body}" ]
500 then
501 msg_body=$subject
502 subject=""
503 fi
504
505 # Send the POST request and check for errors.
506 #
507 msg_out="$( \
508 curl \
509 --form-string "token=${ZED_PUSHOVER_TOKEN}" \
510 --form-string "user=${ZED_PUSHOVER_USER}" \
511 --form-string "message=${msg_body}" \
512 --form-string "title=${subject}" \
513 "${url}" \
514 2>/dev/null \
515 )"; rv=$?
516 if [ "${rv}" -ne 0 ]; then
517 zed_log_err "curl exit=${rv}"
518 return 1
519 fi
520 msg_err="$(echo "${msg_out}" \
521 | sed -n -e 's/.*"errors" *:.*\[\(.*\)\].*/\1/p')"
522 if [ -n "${msg_err}" ]; then
523 zed_log_err "pushover \"${msg_err}"\"
524 return 1
525 fi
526 return 0
527 }
528
529
530 # zed_rate_limit (tag, [interval])
531 #
532 # Check whether an event of a given type [tag] has already occurred within the
533 # last [interval] seconds.
534 #
535 # This function obtains a lock on the statefile using file descriptor 9.
536 #
537 # Arguments
538 # tag: arbitrary string for grouping related events to rate-limit
539 # interval: time interval in seconds (OPTIONAL)
540 #
541 # Globals
542 # ZED_NOTIFY_INTERVAL_SECS
543 # ZED_RUNDIR
544 #
545 # Return
546 # 0 if the event should be processed
547 # 1 if the event should be dropped
548 #
549 # State File Format
550 # time;tag
551 #
552 zed_rate_limit()
553 {
554 local tag="$1"
555 local interval="${2:-${ZED_NOTIFY_INTERVAL_SECS}}"
556 local lockfile="zed.zedlet.state.lock"
557 local lockfile_fd=9
558 local statefile="${ZED_RUNDIR}/zed.zedlet.state"
559 local time_now
560 local time_prev
561 local umask_bak
562 local rv=0
563
564 [ -n "${tag}" ] || return 0
565
566 zed_lock "${lockfile}" "${lockfile_fd}"
567 time_now="$(date +%s)"
568 time_prev="$(grep -E "^[0-9]+;${tag}\$" "${statefile}" 2>/dev/null \
569 | tail -1 | cut -d\; -f1)"
570
571 if [ -n "${time_prev}" ] \
572 && [ "$((time_now - time_prev))" -lt "${interval}" ]; then
573 rv=1
574 else
575 umask_bak="$(umask)"
576 umask 077
577 grep -E -v "^[0-9]+;${tag}\$" "${statefile}" 2>/dev/null \
578 > "${statefile}.$$"
579 echo "${time_now};${tag}" >> "${statefile}.$$"
580 mv -f "${statefile}.$$" "${statefile}"
581 umask "${umask_bak}"
582 fi
583
584 zed_unlock "${lockfile}" "${lockfile_fd}"
585 return "${rv}"
586 }
587
588
589 # zed_guid_to_pool (guid)
590 #
591 # Convert a pool GUID into its pool name (like "tank")
592 # Arguments
593 # guid: pool GUID (decimal or hex)
594 #
595 # Return
596 # Pool name
597 #
598 zed_guid_to_pool()
599 {
600 if [ -z "$1" ] ; then
601 return
602 fi
603
604 guid="$(printf "%u" "$1")"
605 $ZPOOL get -H -ovalue,name guid | awk '$1 == '"$guid"' {print $2; exit}'
606 }
607
608 # zed_exit_if_ignoring_this_event
609 #
610 # Exit the script if we should ignore this event, as determined by
611 # $ZED_SYSLOG_SUBCLASS_INCLUDE and $ZED_SYSLOG_SUBCLASS_EXCLUDE in zed.rc.
612 # This function assumes you've imported the normal zed variables.
613 zed_exit_if_ignoring_this_event()
614 {
615 if [ -n "${ZED_SYSLOG_SUBCLASS_INCLUDE}" ]; then
616 eval "case ${ZEVENT_SUBCLASS} in
617 ${ZED_SYSLOG_SUBCLASS_INCLUDE});;
618 *) exit 0;;
619 esac"
620 elif [ -n "${ZED_SYSLOG_SUBCLASS_EXCLUDE}" ]; then
621 eval "case ${ZEVENT_SUBCLASS} in
622 ${ZED_SYSLOG_SUBCLASS_EXCLUDE}) exit 0;;
623 *);;
624 esac"
625 fi
626 }
Cache object: d5a7c6697d79c4eb09f4d40e556a922f
|