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