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