]> granicus.if.org Git - zfs/blob - cmd/zed/zed.d/zed-functions.sh
Adding slack notifier
[zfs] / cmd / zed / zed.d / zed-functions.sh
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
17 ZED_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 #
32 zed_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 #
58 zed_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 #
77 zed_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 #
112 zed_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 #
156 zed_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 #
190 zed_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 #
242 zed_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 #
308 zed_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 #
389 zed_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 #
462 zed_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 #
508 zed_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.
525 zed_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 }