xref: /freebsd/sys/contrib/openzfs/cmd/zed/zed.d/zed-functions.sh (revision 9e5787d2284e187abb5b654d924394a65772e004)
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    err="$(flock --exclusive "${fd}" 2>&1)"
131    # shellcheck disable=SC2181
132    if [ $? -ne 0 ]; then
133        zed_log_err "failed to lock \"${lockfile}\": ${err}"
134    fi
135
136    umask "${umask_bak}"
137}
138
139
140# zed_unlock (lockfile, [fd])
141#
142# Release the lock on [lockfile].
143#
144# Arguments
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)
148#
149# Globals
150#   ZED_FLOCK_FD
151#   ZED_LOCKDIR
152#
153# Return
154#   nothing
155#
156zed_unlock()
157{
158    local lockfile="$1"
159    local fd="${2:-${ZED_FLOCK_FD}}"
160    local err
161
162    [ -n "${lockfile}" ] || return
163    if ! expr "${lockfile}" : '.*/' >/dev/null 2>&1; then
164        lockfile="${ZED_LOCKDIR}/${lockfile}"
165    fi
166
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}"
172    fi
173    eval "exec ${fd}>&-"
174}
175
176
177# zed_notify (subject, pathname)
178#
179# Send a notification via all available methods.
180#
181# Arguments
182#   subject: notification subject
183#   pathname: pathname containing the notification message (OPTIONAL)
184#
185# Return
186#   0: notification succeeded via at least one method
187#   1: notification failed
188#   2: no notification methods configured
189#
190zed_notify()
191{
192    local subject="$1"
193    local pathname="$2"
194    local num_success=0
195    local num_failure=0
196
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))
200
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))
204
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))
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_rate_limit (tag, [interval])
441#
442# Check whether an event of a given type [tag] has already occurred within the
443# last [interval] seconds.
444#
445# This function obtains a lock on the statefile using file descriptor 9.
446#
447# Arguments
448#   tag: arbitrary string for grouping related events to rate-limit
449#   interval: time interval in seconds (OPTIONAL)
450#
451# Globals
452#   ZED_NOTIFY_INTERVAL_SECS
453#   ZED_RUNDIR
454#
455# Return
456#   0 if the event should be processed
457#   1 if the event should be dropped
458#
459# State File Format
460#   time;tag
461#
462zed_rate_limit()
463{
464    local tag="$1"
465    local interval="${2:-${ZED_NOTIFY_INTERVAL_SECS}}"
466    local lockfile="zed.zedlet.state.lock"
467    local lockfile_fd=9
468    local statefile="${ZED_RUNDIR}/zed.zedlet.state"
469    local time_now
470    local time_prev
471    local umask_bak
472    local rv=0
473
474    [ -n "${tag}" ] || return 0
475
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)"
480
481    if [ -n "${time_prev}" ] \
482            && [ "$((time_now - time_prev))" -lt "${interval}" ]; then
483        rv=1
484    else
485        umask_bak="$(umask)"
486        umask 077
487        grep -E -v "^[0-9]+;${tag}\$" "${statefile}" 2>/dev/null \
488            > "${statefile}.$$"
489        echo "${time_now};${tag}" >> "${statefile}.$$"
490        mv -f "${statefile}.$$" "${statefile}"
491        umask "${umask_bak}"
492    fi
493
494    zed_unlock "${lockfile}" "${lockfile_fd}"
495    return "${rv}"
496}
497
498
499# zed_guid_to_pool (guid)
500#
501# Convert a pool GUID into its pool name (like "tank")
502# Arguments
503#   guid: pool GUID (decimal or hex)
504#
505# Return
506#   Pool name
507#
508zed_guid_to_pool()
509{
510	if [ -z "$1" ] ; then
511		return
512	fi
513
514	guid=$(printf "%llu" "$1")
515	if [ -n "$guid" ] ; then
516		$ZPOOL get -H -ovalue,name guid | awk '$1=='"$guid"' {print $2}'
517	fi
518}
519
520# zed_exit_if_ignoring_this_event
521#
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.
525zed_exit_if_ignoring_this_event()
526{
527	if [ -n "${ZED_SYSLOG_SUBCLASS_INCLUDE}" ]; then
528	    eval "case ${ZEVENT_SUBCLASS} in
529	    ${ZED_SYSLOG_SUBCLASS_INCLUDE});;
530	    *) exit 0;;
531	    esac"
532	elif [ -n "${ZED_SYSLOG_SUBCLASS_EXCLUDE}" ]; then
533	    eval "case ${ZEVENT_SUBCLASS} in
534	    ${ZED_SYSLOG_SUBCLASS_EXCLUDE}) exit 0;;
535	    *);;
536	    esac"
537	fi
538}
539