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