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
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    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#
111zed_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#
153zed_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#
185zed_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#
243zed_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#
320zed_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#
401zed_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#
477zed_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#
552zed_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#
598zed_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.
613zed_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}
627