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 zed_notify_pushover "${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_notify_pushover (subject, pathname) 441# 442# Send a notification via Pushover <https://pushover.net/>. 443# The access token (ZED_PUSHOVER_TOKEN) identifies this client to the 444# Pushover server. The user token (ZED_PUSHOVER_USER) defines the user or 445# group to which the notification will be sent. 446# 447# Requires curl and sed executables to be installed in the standard PATH. 448# 449# References 450# https://pushover.net/api 451# 452# Arguments 453# subject: notification subject 454# pathname: pathname containing the notification message (OPTIONAL) 455# 456# Globals 457# ZED_PUSHOVER_TOKEN 458# ZED_PUSHOVER_USER 459# 460# Return 461# 0: notification sent 462# 1: notification failed 463# 2: not configured 464# 465zed_notify_pushover() 466{ 467 local subject="$1" 468 local pathname="${2:-"/dev/null"}" 469 local msg_body 470 local msg_out 471 local msg_err 472 local url="https://api.pushover.net/1/messages.json" 473 474 [ -n "${ZED_PUSHOVER_TOKEN}" ] && [ -n "${ZED_PUSHOVER_USER}" ] || return 2 475 476 if [ ! -r "${pathname}" ]; then 477 zed_log_err "pushover cannot read \"${pathname}\"" 478 return 1 479 fi 480 481 zed_check_cmd "curl" "sed" || return 1 482 483 # Read the message body in. 484 # 485 msg_body="$(cat "${pathname}")" 486 487 if [ -z "${msg_body}" ] 488 then 489 msg_body=$subject 490 subject="" 491 fi 492 493 # Send the POST request and check for errors. 494 # 495 msg_out="$( \ 496 curl \ 497 --form-string "token=${ZED_PUSHOVER_TOKEN}" \ 498 --form-string "user=${ZED_PUSHOVER_USER}" \ 499 --form-string "message=${msg_body}" \ 500 --form-string "title=${subject}" \ 501 "${url}" \ 502 2>/dev/null \ 503 )"; rv=$? 504 if [ "${rv}" -ne 0 ]; then 505 zed_log_err "curl exit=${rv}" 506 return 1 507 fi 508 msg_err="$(echo "${msg_out}" \ 509 | sed -n -e 's/.*"errors" *:.*\[\(.*\)\].*/\1/p')" 510 if [ -n "${msg_err}" ]; then 511 zed_log_err "pushover \"${msg_err}"\" 512 return 1 513 fi 514 return 0 515} 516 517 518# zed_rate_limit (tag, [interval]) 519# 520# Check whether an event of a given type [tag] has already occurred within the 521# last [interval] seconds. 522# 523# This function obtains a lock on the statefile using file descriptor 9. 524# 525# Arguments 526# tag: arbitrary string for grouping related events to rate-limit 527# interval: time interval in seconds (OPTIONAL) 528# 529# Globals 530# ZED_NOTIFY_INTERVAL_SECS 531# ZED_RUNDIR 532# 533# Return 534# 0 if the event should be processed 535# 1 if the event should be dropped 536# 537# State File Format 538# time;tag 539# 540zed_rate_limit() 541{ 542 local tag="$1" 543 local interval="${2:-${ZED_NOTIFY_INTERVAL_SECS}}" 544 local lockfile="zed.zedlet.state.lock" 545 local lockfile_fd=9 546 local statefile="${ZED_RUNDIR}/zed.zedlet.state" 547 local time_now 548 local time_prev 549 local umask_bak 550 local rv=0 551 552 [ -n "${tag}" ] || return 0 553 554 zed_lock "${lockfile}" "${lockfile_fd}" 555 time_now="$(date +%s)" 556 time_prev="$(grep -E "^[0-9]+;${tag}\$" "${statefile}" 2>/dev/null \ 557 | tail -1 | cut -d\; -f1)" 558 559 if [ -n "${time_prev}" ] \ 560 && [ "$((time_now - time_prev))" -lt "${interval}" ]; then 561 rv=1 562 else 563 umask_bak="$(umask)" 564 umask 077 565 grep -E -v "^[0-9]+;${tag}\$" "${statefile}" 2>/dev/null \ 566 > "${statefile}.$$" 567 echo "${time_now};${tag}" >> "${statefile}.$$" 568 mv -f "${statefile}.$$" "${statefile}" 569 umask "${umask_bak}" 570 fi 571 572 zed_unlock "${lockfile}" "${lockfile_fd}" 573 return "${rv}" 574} 575 576 577# zed_guid_to_pool (guid) 578# 579# Convert a pool GUID into its pool name (like "tank") 580# Arguments 581# guid: pool GUID (decimal or hex) 582# 583# Return 584# Pool name 585# 586zed_guid_to_pool() 587{ 588 if [ -z "$1" ] ; then 589 return 590 fi 591 592 guid="$(printf "%u" "$1")" 593 $ZPOOL get -H -ovalue,name guid | awk '$1 == '"$guid"' {print $2; exit}' 594} 595 596# zed_exit_if_ignoring_this_event 597# 598# Exit the script if we should ignore this event, as determined by 599# $ZED_SYSLOG_SUBCLASS_INCLUDE and $ZED_SYSLOG_SUBCLASS_EXCLUDE in zed.rc. 600# This function assumes you've imported the normal zed variables. 601zed_exit_if_ignoring_this_event() 602{ 603 if [ -n "${ZED_SYSLOG_SUBCLASS_INCLUDE}" ]; then 604 eval "case ${ZEVENT_SUBCLASS} in 605 ${ZED_SYSLOG_SUBCLASS_INCLUDE});; 606 *) exit 0;; 607 esac" 608 elif [ -n "${ZED_SYSLOG_SUBCLASS_EXCLUDE}" ]; then 609 eval "case ${ZEVENT_SUBCLASS} in 610 ${ZED_SYSLOG_SUBCLASS_EXCLUDE}) exit 0;; 611 *);; 612 esac" 613 fi 614} 615