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