2 # shellcheck disable=SC2039
5 # ZED helper functions for use in ZEDLETs
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"}"
20 # zed_check_cmd (cmd, ...)
22 # For each argument given, search PATH for the executable command [cmd].
23 # Log a message if [cmd] is not found.
26 # cmd: name of executable command for which to search
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
38 if ! command -v "${cmd}" >/dev/null 2>&1; then
39 zed_log_err "\"${cmd}\" not installed"
47 # zed_log_msg (msg, ...)
49 # Write all argument strings to the system log.
60 logger -p "${ZED_SYSLOG_PRIORITY}" -t "${ZED_SYSLOG_TAG}" -- "$@"
64 # zed_log_err (msg, ...)
66 # Write an error message to the system log. This message will contain the
67 # script name, EID, and all argument strings.
79 logger -p "${ZED_SYSLOG_PRIORITY}" -t "${ZED_SYSLOG_TAG}" -- "error:" \
80 "$(basename -- "$0"):""${ZEVENT_EID:+" eid=${ZEVENT_EID}:"}" "$@"
84 # zed_lock (lockfile, [fd])
86 # Obtain an exclusive (write) lock on [lockfile]. If the lock cannot be
87 # immediately acquired, wait until it becomes available.
89 # Every zed_lock() must be paired with a corresponding zed_unlock().
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.
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
115 local fd="${2:-${ZED_FLOCK_FD}}"
119 [ -n "${lockfile}" ] || return
120 if ! expr "${lockfile}" : '.*/' >/dev/null 2>&1; then
121 lockfile="${ZED_LOCKDIR}/${lockfile}"
127 # Obtain a lock on the file bound to the given file descriptor.
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}"
140 # zed_unlock (lockfile, [fd])
142 # Release the lock on [lockfile].
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)
159 local fd="${2:-${ZED_FLOCK_FD}}"
162 [ -n "${lockfile}" ] || return
163 if ! expr "${lockfile}" : '.*/' >/dev/null 2>&1; then
164 lockfile="${ZED_LOCKDIR}/${lockfile}"
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}"
177 # zed_notify (subject, pathname)
179 # Send a notification via all available methods.
182 # subject: notification subject
183 # pathname: pathname containing the notification message (OPTIONAL)
186 # 0: notification succeeded via at least one method
187 # 1: notification failed
188 # 2: no notification methods configured
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))
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))
205 zed_notify_slack_webhook "${subject}" "${pathname}"; rv=$?
206 [ "${rv}" -eq 0 ] && num_success=$((num_success + 1))
207 [ "${rv}" -eq 1 ] && num_failure=$((num_failure + 1))
209 [ "${num_success}" -gt 0 ] && return 0
210 [ "${num_failure}" -gt 0 ] && return 1
215 # zed_notify_email (subject, pathname)
217 # Send a notification via email to the address specified by ZED_EMAIL_ADDR.
219 # Requires the mail executable to be installed in the standard PATH, or
220 # ZED_EMAIL_PROG to be defined with the pathname of an executable capable of
221 # reading a message body from stdin.
223 # Command-line options to the mail executable can be specified in
224 # ZED_EMAIL_OPTS. This undergoes the following keyword substitutions:
225 # - @ADDRESS@ is replaced with the space-delimited recipient email address(es)
226 # - @SUBJECT@ is replaced with the notification subject
229 # subject: notification subject
230 # pathname: pathname containing the notification message (OPTIONAL)
238 # 0: notification sent
239 # 1: notification failed
245 local pathname="${2:-"/dev/null"}"
247 : "${ZED_EMAIL_PROG:="mail"}"
248 : "${ZED_EMAIL_OPTS:="-s '@SUBJECT@' @ADDRESS@"}"
250 # For backward compatibility with ZED_EMAIL.
251 if [ -n "${ZED_EMAIL}" ] && [ -z "${ZED_EMAIL_ADDR}" ]; then
252 ZED_EMAIL_ADDR="${ZED_EMAIL}"
254 [ -n "${ZED_EMAIL_ADDR}" ] || return 2
256 zed_check_cmd "${ZED_EMAIL_PROG}" || return 1
258 [ -n "${subject}" ] || return 1
259 if [ ! -r "${pathname}" ]; then
261 "$(basename "${ZED_EMAIL_PROG}") cannot read \"${pathname}\""
265 ZED_EMAIL_OPTS="$(echo "${ZED_EMAIL_OPTS}" \
266 | sed -e "s/@ADDRESS@/${ZED_EMAIL_ADDR}/g" \
267 -e "s/@SUBJECT@/${subject}/g")"
269 # shellcheck disable=SC2086
270 eval "${ZED_EMAIL_PROG}" ${ZED_EMAIL_OPTS} < "${pathname}" >/dev/null 2>&1
272 if [ "${rv}" -ne 0 ]; then
273 zed_log_err "$(basename "${ZED_EMAIL_PROG}") exit=${rv}"
280 # zed_notify_pushbullet (subject, pathname)
282 # Send a notification via Pushbullet <https://www.pushbullet.com/>.
283 # The access token (ZED_PUSHBULLET_ACCESS_TOKEN) identifies this client to the
284 # Pushbullet server. The optional channel tag (ZED_PUSHBULLET_CHANNEL_TAG) is
285 # for pushing to notification feeds that can be subscribed to; if a channel is
286 # not defined, push notifications will instead be sent to all devices
287 # associated with the account specified by the access token.
289 # Requires awk, curl, and sed executables to be installed in the standard PATH.
292 # https://docs.pushbullet.com/
293 # https://www.pushbullet.com/security
296 # subject: notification subject
297 # pathname: pathname containing the notification message (OPTIONAL)
300 # ZED_PUSHBULLET_ACCESS_TOKEN
301 # ZED_PUSHBULLET_CHANNEL_TAG
304 # 0: notification sent
305 # 1: notification failed
308 zed_notify_pushbullet()
311 local pathname="${2:-"/dev/null"}"
317 local url="https://api.pushbullet.com/v2/pushes"
319 [ -n "${ZED_PUSHBULLET_ACCESS_TOKEN}" ] || return 2
321 [ -n "${subject}" ] || return 1
322 if [ ! -r "${pathname}" ]; then
323 zed_log_err "pushbullet cannot read \"${pathname}\""
327 zed_check_cmd "awk" "curl" "sed" || return 1
329 # Escape the following characters in the message body for JSON:
330 # newline, backslash, double quote, horizontal tab, vertical tab,
331 # and carriage return.
333 msg_body="$(awk '{ ORS="\\n" } { gsub(/\\/, "\\\\"); gsub(/"/, "\\\"");
334 gsub(/\t/, "\\t"); gsub(/\f/, "\\f"); gsub(/\r/, "\\r"); print }' \
337 # Push to a channel if one is configured.
339 [ -n "${ZED_PUSHBULLET_CHANNEL_TAG}" ] && msg_tag="$(printf \
340 '"channel_tag": "%s", ' "${ZED_PUSHBULLET_CHANNEL_TAG}")"
342 # Construct the JSON message for pushing a note.
344 msg_json="$(printf '{%s"type": "note", "title": "%s", "body": "%s"}' \
345 "${msg_tag}" "${subject}" "${msg_body}")"
347 # Send the POST request and check for errors.
349 msg_out="$(curl -u "${ZED_PUSHBULLET_ACCESS_TOKEN}:" -X POST "${url}" \
350 --header "Content-Type: application/json" --data-binary "${msg_json}" \
352 if [ "${rv}" -ne 0 ]; then
353 zed_log_err "curl exit=${rv}"
356 msg_err="$(echo "${msg_out}" \
357 | sed -n -e 's/.*"error" *:.*"message" *: *"\([^"]*\)".*/\1/p')"
358 if [ -n "${msg_err}" ]; then
359 zed_log_err "pushbullet \"${msg_err}"\"
366 # zed_notify_slack_webhook (subject, pathname)
368 # Notification via Slack Webhook <https://api.slack.com/incoming-webhooks>.
369 # The Webhook URL (ZED_SLACK_WEBHOOK_URL) identifies this client to the
372 # Requires awk, curl, and sed executables to be installed in the standard PATH.
375 # https://api.slack.com/incoming-webhooks
378 # subject: notification subject
379 # pathname: pathname containing the notification message (OPTIONAL)
382 # ZED_SLACK_WEBHOOK_URL
385 # 0: notification sent
386 # 1: notification failed
389 zed_notify_slack_webhook()
391 [ -n "${ZED_SLACK_WEBHOOK_URL}" ] || return 2
394 local pathname="${2:-"/dev/null"}"
400 local url="${ZED_SLACK_WEBHOOK_URL}"
402 [ -n "${subject}" ] || return 1
403 if [ ! -r "${pathname}" ]; then
404 zed_log_err "slack webhook cannot read \"${pathname}\""
408 zed_check_cmd "awk" "curl" "sed" || return 1
410 # Escape the following characters in the message body for JSON:
411 # newline, backslash, double quote, horizontal tab, vertical tab,
412 # and carriage return.
414 msg_body="$(awk '{ ORS="\\n" } { gsub(/\\/, "\\\\"); gsub(/"/, "\\\"");
415 gsub(/\t/, "\\t"); gsub(/\f/, "\\f"); gsub(/\r/, "\\r"); print }' \
418 # Construct the JSON message for posting.
420 msg_json="$(printf '{"text": "*%s*\n%s"}' "${subject}" "${msg_body}" )"
422 # Send the POST request and check for errors.
424 msg_out="$(curl -X POST "${url}" \
425 --header "Content-Type: application/json" --data-binary "${msg_json}" \
427 if [ "${rv}" -ne 0 ]; then
428 zed_log_err "curl exit=${rv}"
431 msg_err="$(echo "${msg_out}" \
432 | sed -n -e 's/.*"error" *:.*"message" *: *"\([^"]*\)".*/\1/p')"
433 if [ -n "${msg_err}" ]; then
434 zed_log_err "slack webhook \"${msg_err}"\"
440 # zed_rate_limit (tag, [interval])
442 # Check whether an event of a given type [tag] has already occurred within the
443 # last [interval] seconds.
445 # This function obtains a lock on the statefile using file descriptor 9.
448 # tag: arbitrary string for grouping related events to rate-limit
449 # interval: time interval in seconds (OPTIONAL)
452 # ZED_NOTIFY_INTERVAL_SECS
456 # 0 if the event should be processed
457 # 1 if the event should be dropped
465 local interval="${2:-${ZED_NOTIFY_INTERVAL_SECS}}"
466 local lockfile="zed.zedlet.state.lock"
468 local statefile="${ZED_RUNDIR}/zed.zedlet.state"
474 [ -n "${tag}" ] || return 0
476 zed_lock "${lockfile}" "${lockfile_fd}"
477 time_now="$(date +%s)"
478 time_prev="$(grep -E "^[0-9]+;${tag}\$" "${statefile}" 2>/dev/null \
479 | tail -1 | cut -d\; -f1)"
481 if [ -n "${time_prev}" ] \
482 && [ "$((time_now - time_prev))" -lt "${interval}" ]; then
487 grep -E -v "^[0-9]+;${tag}\$" "${statefile}" 2>/dev/null \
489 echo "${time_now};${tag}" >> "${statefile}.$$"
490 mv -f "${statefile}.$$" "${statefile}"
494 zed_unlock "${lockfile}" "${lockfile_fd}"
499 # zed_guid_to_pool (guid)
501 # Convert a pool GUID into its pool name (like "tank")
503 # guid: pool GUID (decimal or hex)
510 if [ -z "$1" ] ; then
514 guid=$(printf "%llu" "$1")
515 if [ -n "$guid" ] ; then
516 $ZPOOL get -H -ovalue,name guid | awk '$1=='"$guid"' {print $2}'
520 # zed_exit_if_ignoring_this_event
522 # Exit the script if we should ignore this event, as determined by
523 # $ZED_SYSLOG_SUBCLASS_INCLUDE and $ZED_SYSLOG_SUBCLASS_EXCLUDE in zed.rc.
524 # This function assumes you've imported the normal zed variables.
525 zed_exit_if_ignoring_this_event()
527 if [ -n "${ZED_SYSLOG_SUBCLASS_INCLUDE}" ]; then
528 eval "case ${ZEVENT_SUBCLASS} in
529 ${ZED_SYSLOG_SUBCLASS_INCLUDE});;
532 elif [ -n "${ZED_SYSLOG_SUBCLASS_EXCLUDE}" ]; then
533 eval "case ${ZEVENT_SUBCLASS} in
534 ${ZED_SYSLOG_SUBCLASS_EXCLUDE}) exit 0;;