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