538 lines
14 KiB
Bash
538 lines
14 KiB
Bash
#!/bin/sh
|
|
# shellcheck disable=SC2039
|
|
# zed-functions.sh
|
|
#
|
|
# ZED helper functions for use in ZEDLETs
|
|
|
|
|
|
# Variable Defaults
|
|
#
|
|
: "${ZED_LOCKDIR:="/var/lock"}"
|
|
: "${ZED_NOTIFY_INTERVAL_SECS:=3600}"
|
|
: "${ZED_NOTIFY_VERBOSE:=0}"
|
|
: "${ZED_RUNDIR:="/var/run"}"
|
|
: "${ZED_SYSLOG_PRIORITY:="daemon.notice"}"
|
|
: "${ZED_SYSLOG_TAG:="zed"}"
|
|
|
|
ZED_FLOCK_FD=8
|
|
|
|
|
|
# zed_check_cmd (cmd, ...)
|
|
#
|
|
# For each argument given, search PATH for the executable command [cmd].
|
|
# Log a message if [cmd] is not found.
|
|
#
|
|
# Arguments
|
|
# cmd: name of executable command for which to search
|
|
#
|
|
# Return
|
|
# 0 if all commands are found in PATH and are executable
|
|
# n for a count of the command executables that are not found
|
|
#
|
|
zed_check_cmd()
|
|
{
|
|
local cmd
|
|
local rv=0
|
|
|
|
for cmd; do
|
|
if ! command -v "${cmd}" >/dev/null 2>&1; then
|
|
zed_log_err "\"${cmd}\" not installed"
|
|
rv=$((rv + 1))
|
|
fi
|
|
done
|
|
return "${rv}"
|
|
}
|
|
|
|
|
|
# zed_log_msg (msg, ...)
|
|
#
|
|
# Write all argument strings to the system log.
|
|
#
|
|
# Globals
|
|
# ZED_SYSLOG_PRIORITY
|
|
# ZED_SYSLOG_TAG
|
|
#
|
|
# Return
|
|
# nothing
|
|
#
|
|
zed_log_msg()
|
|
{
|
|
logger -p "${ZED_SYSLOG_PRIORITY}" -t "${ZED_SYSLOG_TAG}" -- "$@"
|
|
}
|
|
|
|
|
|
# zed_log_err (msg, ...)
|
|
#
|
|
# Write an error message to the system log. This message will contain the
|
|
# script name, EID, and all argument strings.
|
|
#
|
|
# Globals
|
|
# ZED_SYSLOG_PRIORITY
|
|
# ZED_SYSLOG_TAG
|
|
# ZEVENT_EID
|
|
#
|
|
# Return
|
|
# nothing
|
|
#
|
|
zed_log_err()
|
|
{
|
|
logger -p "${ZED_SYSLOG_PRIORITY}" -t "${ZED_SYSLOG_TAG}" -- "error:" \
|
|
"$(basename -- "$0"):""${ZEVENT_EID:+" eid=${ZEVENT_EID}:"}" "$@"
|
|
}
|
|
|
|
|
|
# zed_lock (lockfile, [fd])
|
|
#
|
|
# Obtain an exclusive (write) lock on [lockfile]. If the lock cannot be
|
|
# immediately acquired, wait until it becomes available.
|
|
#
|
|
# Every zed_lock() must be paired with a corresponding zed_unlock().
|
|
#
|
|
# By default, flock-style locks associate the lockfile with file descriptor 8.
|
|
# The bash manpage warns that file descriptors >9 should be used with care as
|
|
# they may conflict with file descriptors used internally by the shell. File
|
|
# descriptor 9 is reserved for zed_rate_limit(). If concurrent locks are held
|
|
# within the same process, they must use different file descriptors (preferably
|
|
# decrementing from 8); otherwise, obtaining a new lock with a given file
|
|
# descriptor will release the previous lock associated with that descriptor.
|
|
#
|
|
# Arguments
|
|
# lockfile: pathname of the lock file; the lock will be stored in
|
|
# ZED_LOCKDIR unless the pathname contains a "/".
|
|
# fd: integer for the file descriptor used by flock (OPTIONAL unless holding
|
|
# concurrent locks)
|
|
#
|
|
# Globals
|
|
# ZED_FLOCK_FD
|
|
# ZED_LOCKDIR
|
|
#
|
|
# Return
|
|
# nothing
|
|
#
|
|
zed_lock()
|
|
{
|
|
local lockfile="$1"
|
|
local fd="${2:-${ZED_FLOCK_FD}}"
|
|
local umask_bak
|
|
local err
|
|
|
|
[ -n "${lockfile}" ] || return
|
|
if ! expr "${lockfile}" : '.*/' >/dev/null 2>&1; then
|
|
lockfile="${ZED_LOCKDIR}/${lockfile}"
|
|
fi
|
|
|
|
umask_bak="$(umask)"
|
|
umask 077
|
|
|
|
# Obtain a lock on the file bound to the given file descriptor.
|
|
#
|
|
eval "exec ${fd}> '${lockfile}'"
|
|
err="$(flock --exclusive "${fd}" 2>&1)"
|
|
# shellcheck disable=SC2181
|
|
if [ $? -ne 0 ]; then
|
|
zed_log_err "failed to lock \"${lockfile}\": ${err}"
|
|
fi
|
|
|
|
umask "${umask_bak}"
|
|
}
|
|
|
|
|
|
# zed_unlock (lockfile, [fd])
|
|
#
|
|
# Release the lock on [lockfile].
|
|
#
|
|
# Arguments
|
|
# lockfile: pathname of the lock file
|
|
# fd: integer for the file descriptor used by flock (must match the file
|
|
# descriptor passed to the zed_lock function call)
|
|
#
|
|
# Globals
|
|
# ZED_FLOCK_FD
|
|
# ZED_LOCKDIR
|
|
#
|
|
# Return
|
|
# nothing
|
|
#
|
|
zed_unlock()
|
|
{
|
|
local lockfile="$1"
|
|
local fd="${2:-${ZED_FLOCK_FD}}"
|
|
local err
|
|
|
|
[ -n "${lockfile}" ] || return
|
|
if ! expr "${lockfile}" : '.*/' >/dev/null 2>&1; then
|
|
lockfile="${ZED_LOCKDIR}/${lockfile}"
|
|
fi
|
|
|
|
# Release the lock and close the file descriptor.
|
|
err="$(flock --unlock "${fd}" 2>&1)"
|
|
# shellcheck disable=SC2181
|
|
if [ $? -ne 0 ]; then
|
|
zed_log_err "failed to unlock \"${lockfile}\": ${err}"
|
|
fi
|
|
eval "exec ${fd}>&-"
|
|
}
|
|
|
|
|
|
# zed_notify (subject, pathname)
|
|
#
|
|
# Send a notification via all available methods.
|
|
#
|
|
# Arguments
|
|
# subject: notification subject
|
|
# pathname: pathname containing the notification message (OPTIONAL)
|
|
#
|
|
# Return
|
|
# 0: notification succeeded via at least one method
|
|
# 1: notification failed
|
|
# 2: no notification methods configured
|
|
#
|
|
zed_notify()
|
|
{
|
|
local subject="$1"
|
|
local pathname="$2"
|
|
local num_success=0
|
|
local num_failure=0
|
|
|
|
zed_notify_email "${subject}" "${pathname}"; rv=$?
|
|
[ "${rv}" -eq 0 ] && num_success=$((num_success + 1))
|
|
[ "${rv}" -eq 1 ] && num_failure=$((num_failure + 1))
|
|
|
|
zed_notify_pushbullet "${subject}" "${pathname}"; rv=$?
|
|
[ "${rv}" -eq 0 ] && num_success=$((num_success + 1))
|
|
[ "${rv}" -eq 1 ] && num_failure=$((num_failure + 1))
|
|
|
|
zed_notify_slack_webhook "${subject}" "${pathname}"; rv=$?
|
|
[ "${rv}" -eq 0 ] && num_success=$((num_success + 1))
|
|
[ "${rv}" -eq 1 ] && num_failure=$((num_failure + 1))
|
|
|
|
[ "${num_success}" -gt 0 ] && return 0
|
|
[ "${num_failure}" -gt 0 ] && return 1
|
|
return 2
|
|
}
|
|
|
|
|
|
# zed_notify_email (subject, pathname)
|
|
#
|
|
# Send a notification via email to the address specified by ZED_EMAIL_ADDR.
|
|
#
|
|
# Requires the mail executable to be installed in the standard PATH, or
|
|
# ZED_EMAIL_PROG to be defined with the pathname of an executable capable of
|
|
# reading a message body from stdin.
|
|
#
|
|
# Command-line options to the mail executable can be specified in
|
|
# ZED_EMAIL_OPTS. This undergoes the following keyword substitutions:
|
|
# - @ADDRESS@ is replaced with the space-delimited recipient email address(es)
|
|
# - @SUBJECT@ is replaced with the notification subject
|
|
#
|
|
# Arguments
|
|
# subject: notification subject
|
|
# pathname: pathname containing the notification message (OPTIONAL)
|
|
#
|
|
# Globals
|
|
# ZED_EMAIL_PROG
|
|
# ZED_EMAIL_OPTS
|
|
# ZED_EMAIL_ADDR
|
|
#
|
|
# Return
|
|
# 0: notification sent
|
|
# 1: notification failed
|
|
# 2: not configured
|
|
#
|
|
zed_notify_email()
|
|
{
|
|
local subject="$1"
|
|
local pathname="${2:-"/dev/null"}"
|
|
|
|
: "${ZED_EMAIL_PROG:="mail"}"
|
|
: "${ZED_EMAIL_OPTS:="-s '@SUBJECT@' @ADDRESS@"}"
|
|
|
|
# For backward compatibility with ZED_EMAIL.
|
|
if [ -n "${ZED_EMAIL}" ] && [ -z "${ZED_EMAIL_ADDR}" ]; then
|
|
ZED_EMAIL_ADDR="${ZED_EMAIL}"
|
|
fi
|
|
[ -n "${ZED_EMAIL_ADDR}" ] || return 2
|
|
|
|
zed_check_cmd "${ZED_EMAIL_PROG}" || return 1
|
|
|
|
[ -n "${subject}" ] || return 1
|
|
if [ ! -r "${pathname}" ]; then
|
|
zed_log_err \
|
|
"$(basename "${ZED_EMAIL_PROG}") cannot read \"${pathname}\""
|
|
return 1
|
|
fi
|
|
|
|
ZED_EMAIL_OPTS="$(echo "${ZED_EMAIL_OPTS}" \
|
|
| sed -e "s/@ADDRESS@/${ZED_EMAIL_ADDR}/g" \
|
|
-e "s/@SUBJECT@/${subject}/g")"
|
|
|
|
# shellcheck disable=SC2086
|
|
eval "${ZED_EMAIL_PROG}" ${ZED_EMAIL_OPTS} < "${pathname}" >/dev/null 2>&1
|
|
rv=$?
|
|
if [ "${rv}" -ne 0 ]; then
|
|
zed_log_err "$(basename "${ZED_EMAIL_PROG}") exit=${rv}"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
|
|
# zed_notify_pushbullet (subject, pathname)
|
|
#
|
|
# Send a notification via Pushbullet <https://www.pushbullet.com/>.
|
|
# The access token (ZED_PUSHBULLET_ACCESS_TOKEN) identifies this client to the
|
|
# Pushbullet server. The optional channel tag (ZED_PUSHBULLET_CHANNEL_TAG) is
|
|
# for pushing to notification feeds that can be subscribed to; if a channel is
|
|
# not defined, push notifications will instead be sent to all devices
|
|
# associated with the account specified by the access token.
|
|
#
|
|
# Requires awk, curl, and sed executables to be installed in the standard PATH.
|
|
#
|
|
# References
|
|
# https://docs.pushbullet.com/
|
|
# https://www.pushbullet.com/security
|
|
#
|
|
# Arguments
|
|
# subject: notification subject
|
|
# pathname: pathname containing the notification message (OPTIONAL)
|
|
#
|
|
# Globals
|
|
# ZED_PUSHBULLET_ACCESS_TOKEN
|
|
# ZED_PUSHBULLET_CHANNEL_TAG
|
|
#
|
|
# Return
|
|
# 0: notification sent
|
|
# 1: notification failed
|
|
# 2: not configured
|
|
#
|
|
zed_notify_pushbullet()
|
|
{
|
|
local subject="$1"
|
|
local pathname="${2:-"/dev/null"}"
|
|
local msg_body
|
|
local msg_tag
|
|
local msg_json
|
|
local msg_out
|
|
local msg_err
|
|
local url="https://api.pushbullet.com/v2/pushes"
|
|
|
|
[ -n "${ZED_PUSHBULLET_ACCESS_TOKEN}" ] || return 2
|
|
|
|
[ -n "${subject}" ] || return 1
|
|
if [ ! -r "${pathname}" ]; then
|
|
zed_log_err "pushbullet cannot read \"${pathname}\""
|
|
return 1
|
|
fi
|
|
|
|
zed_check_cmd "awk" "curl" "sed" || return 1
|
|
|
|
# Escape the following characters in the message body for JSON:
|
|
# newline, backslash, double quote, horizontal tab, vertical tab,
|
|
# and carriage return.
|
|
#
|
|
msg_body="$(awk '{ ORS="\\n" } { gsub(/\\/, "\\\\"); gsub(/"/, "\\\"");
|
|
gsub(/\t/, "\\t"); gsub(/\f/, "\\f"); gsub(/\r/, "\\r"); print }' \
|
|
"${pathname}")"
|
|
|
|
# Push to a channel if one is configured.
|
|
#
|
|
[ -n "${ZED_PUSHBULLET_CHANNEL_TAG}" ] && msg_tag="$(printf \
|
|
'"channel_tag": "%s", ' "${ZED_PUSHBULLET_CHANNEL_TAG}")"
|
|
|
|
# Construct the JSON message for pushing a note.
|
|
#
|
|
msg_json="$(printf '{%s"type": "note", "title": "%s", "body": "%s"}' \
|
|
"${msg_tag}" "${subject}" "${msg_body}")"
|
|
|
|
# Send the POST request and check for errors.
|
|
#
|
|
msg_out="$(curl -u "${ZED_PUSHBULLET_ACCESS_TOKEN}:" -X POST "${url}" \
|
|
--header "Content-Type: application/json" --data-binary "${msg_json}" \
|
|
2>/dev/null)"; rv=$?
|
|
if [ "${rv}" -ne 0 ]; then
|
|
zed_log_err "curl exit=${rv}"
|
|
return 1
|
|
fi
|
|
msg_err="$(echo "${msg_out}" \
|
|
| sed -n -e 's/.*"error" *:.*"message" *: *"\([^"]*\)".*/\1/p')"
|
|
if [ -n "${msg_err}" ]; then
|
|
zed_log_err "pushbullet \"${msg_err}"\"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
|
|
# zed_notify_slack_webhook (subject, pathname)
|
|
#
|
|
# Notification via Slack Webhook <https://api.slack.com/incoming-webhooks>.
|
|
# The Webhook URL (ZED_SLACK_WEBHOOK_URL) identifies this client to the
|
|
# Slack channel.
|
|
#
|
|
# Requires awk, curl, and sed executables to be installed in the standard PATH.
|
|
#
|
|
# References
|
|
# https://api.slack.com/incoming-webhooks
|
|
#
|
|
# Arguments
|
|
# subject: notification subject
|
|
# pathname: pathname containing the notification message (OPTIONAL)
|
|
#
|
|
# Globals
|
|
# ZED_SLACK_WEBHOOK_URL
|
|
#
|
|
# Return
|
|
# 0: notification sent
|
|
# 1: notification failed
|
|
# 2: not configured
|
|
#
|
|
zed_notify_slack_webhook()
|
|
{
|
|
[ -n "${ZED_SLACK_WEBHOOK_URL}" ] || return 2
|
|
|
|
local subject="$1"
|
|
local pathname="${2:-"/dev/null"}"
|
|
local msg_body
|
|
local msg_tag
|
|
local msg_json
|
|
local msg_out
|
|
local msg_err
|
|
local url="${ZED_SLACK_WEBHOOK_URL}"
|
|
|
|
[ -n "${subject}" ] || return 1
|
|
if [ ! -r "${pathname}" ]; then
|
|
zed_log_err "slack webhook cannot read \"${pathname}\""
|
|
return 1
|
|
fi
|
|
|
|
zed_check_cmd "awk" "curl" "sed" || return 1
|
|
|
|
# Escape the following characters in the message body for JSON:
|
|
# newline, backslash, double quote, horizontal tab, vertical tab,
|
|
# and carriage return.
|
|
#
|
|
msg_body="$(awk '{ ORS="\\n" } { gsub(/\\/, "\\\\"); gsub(/"/, "\\\"");
|
|
gsub(/\t/, "\\t"); gsub(/\f/, "\\f"); gsub(/\r/, "\\r"); print }' \
|
|
"${pathname}")"
|
|
|
|
# Construct the JSON message for posting.
|
|
#
|
|
msg_json="$(printf '{"text": "*%s*\n%s"}' "${subject}" "${msg_body}" )"
|
|
|
|
# Send the POST request and check for errors.
|
|
#
|
|
msg_out="$(curl -X POST "${url}" \
|
|
--header "Content-Type: application/json" --data-binary "${msg_json}" \
|
|
2>/dev/null)"; rv=$?
|
|
if [ "${rv}" -ne 0 ]; then
|
|
zed_log_err "curl exit=${rv}"
|
|
return 1
|
|
fi
|
|
msg_err="$(echo "${msg_out}" \
|
|
| sed -n -e 's/.*"error" *:.*"message" *: *"\([^"]*\)".*/\1/p')"
|
|
if [ -n "${msg_err}" ]; then
|
|
zed_log_err "slack webhook \"${msg_err}"\"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# zed_rate_limit (tag, [interval])
|
|
#
|
|
# Check whether an event of a given type [tag] has already occurred within the
|
|
# last [interval] seconds.
|
|
#
|
|
# This function obtains a lock on the statefile using file descriptor 9.
|
|
#
|
|
# Arguments
|
|
# tag: arbitrary string for grouping related events to rate-limit
|
|
# interval: time interval in seconds (OPTIONAL)
|
|
#
|
|
# Globals
|
|
# ZED_NOTIFY_INTERVAL_SECS
|
|
# ZED_RUNDIR
|
|
#
|
|
# Return
|
|
# 0 if the event should be processed
|
|
# 1 if the event should be dropped
|
|
#
|
|
# State File Format
|
|
# time;tag
|
|
#
|
|
zed_rate_limit()
|
|
{
|
|
local tag="$1"
|
|
local interval="${2:-${ZED_NOTIFY_INTERVAL_SECS}}"
|
|
local lockfile="zed.zedlet.state.lock"
|
|
local lockfile_fd=9
|
|
local statefile="${ZED_RUNDIR}/zed.zedlet.state"
|
|
local time_now
|
|
local time_prev
|
|
local umask_bak
|
|
local rv=0
|
|
|
|
[ -n "${tag}" ] || return 0
|
|
|
|
zed_lock "${lockfile}" "${lockfile_fd}"
|
|
time_now="$(date +%s)"
|
|
time_prev="$(grep -E "^[0-9]+;${tag}\$" "${statefile}" 2>/dev/null \
|
|
| tail -1 | cut -d\; -f1)"
|
|
|
|
if [ -n "${time_prev}" ] \
|
|
&& [ "$((time_now - time_prev))" -lt "${interval}" ]; then
|
|
rv=1
|
|
else
|
|
umask_bak="$(umask)"
|
|
umask 077
|
|
grep -E -v "^[0-9]+;${tag}\$" "${statefile}" 2>/dev/null \
|
|
> "${statefile}.$$"
|
|
echo "${time_now};${tag}" >> "${statefile}.$$"
|
|
mv -f "${statefile}.$$" "${statefile}"
|
|
umask "${umask_bak}"
|
|
fi
|
|
|
|
zed_unlock "${lockfile}" "${lockfile_fd}"
|
|
return "${rv}"
|
|
}
|
|
|
|
|
|
# zed_guid_to_pool (guid)
|
|
#
|
|
# Convert a pool GUID into its pool name (like "tank")
|
|
# Arguments
|
|
# guid: pool GUID (decimal or hex)
|
|
#
|
|
# Return
|
|
# Pool name
|
|
#
|
|
zed_guid_to_pool()
|
|
{
|
|
if [ -z "$1" ] ; then
|
|
return
|
|
fi
|
|
|
|
guid=$(printf "%llu" "$1")
|
|
if [ -n "$guid" ] ; then
|
|
$ZPOOL get -H -ovalue,name guid | awk '$1=='"$guid"' {print $2}'
|
|
fi
|
|
}
|
|
|
|
# zed_exit_if_ignoring_this_event
|
|
#
|
|
# Exit the script if we should ignore this event, as determined by
|
|
# $ZED_SYSLOG_SUBCLASS_INCLUDE and $ZED_SYSLOG_SUBCLASS_EXCLUDE in zed.rc.
|
|
# This function assumes you've imported the normal zed variables.
|
|
zed_exit_if_ignoring_this_event()
|
|
{
|
|
if [ -n "${ZED_SYSLOG_SUBCLASS_INCLUDE}" ]; then
|
|
eval "case ${ZEVENT_SUBCLASS} in
|
|
${ZED_SYSLOG_SUBCLASS_INCLUDE});;
|
|
*) exit 0;;
|
|
esac"
|
|
elif [ -n "${ZED_SYSLOG_SUBCLASS_EXCLUDE}" ]; then
|
|
eval "case ${ZEVENT_SUBCLASS} in
|
|
${ZED_SYSLOG_SUBCLASS_EXCLUDE}) exit 0;;
|
|
*);;
|
|
esac"
|
|
fi
|
|
}
|