diff --git a/cmd/zed/Makefile.am b/cmd/zed/Makefile.am index 09be2ca6c3ea..f0d22411d6f9 100644 --- a/cmd/zed/Makefile.am +++ b/cmd/zed/Makefile.am @@ -1,71 +1,72 @@ include $(top_srcdir)/config/Rules.am DEFAULT_INCLUDES += \ -I$(top_srcdir)/include \ -I$(top_srcdir)/lib/libspl/include EXTRA_DIST = $(top_srcdir)/cmd/zed/zed.d/README sbin_PROGRAMS = zed zed_SOURCES = \ $(top_srcdir)/cmd/zed/zed.c \ $(top_srcdir)/cmd/zed/zed.h \ $(top_srcdir)/cmd/zed/zed_conf.c \ $(top_srcdir)/cmd/zed/zed_conf.h \ $(top_srcdir)/cmd/zed/zed_event.c \ $(top_srcdir)/cmd/zed/zed_event.h \ $(top_srcdir)/cmd/zed/zed_exec.c \ $(top_srcdir)/cmd/zed/zed_exec.h \ $(top_srcdir)/cmd/zed/zed_file.c \ $(top_srcdir)/cmd/zed/zed_file.h \ $(top_srcdir)/cmd/zed/zed_log.c \ $(top_srcdir)/cmd/zed/zed_log.h \ $(top_srcdir)/cmd/zed/zed_strings.c \ $(top_srcdir)/cmd/zed/zed_strings.h zed_LDADD = \ $(top_builddir)/lib/libavl/libavl.la \ $(top_builddir)/lib/libnvpair/libnvpair.la \ $(top_builddir)/lib/libspl/libspl.la \ $(top_builddir)/lib/libzpool/libzpool.la \ $(top_builddir)/lib/libzfs/libzfs.la \ $(top_builddir)/lib/libzfs_core/libzfs_core.la zedconfdir = $(sysconfdir)/zfs/zed.d dist_zedconf_DATA = \ $(top_srcdir)/cmd/zed/zed.d/zed-functions.sh \ $(top_srcdir)/cmd/zed/zed.d/zed.rc zedexecdir = $(libexecdir)/zfs/zed.d dist_zedexec_SCRIPTS = \ $(top_srcdir)/cmd/zed/zed.d/all-debug.sh \ $(top_srcdir)/cmd/zed/zed.d/all-syslog.sh \ $(top_srcdir)/cmd/zed/zed.d/checksum-notify.sh \ $(top_srcdir)/cmd/zed/zed.d/checksum-spare.sh \ $(top_srcdir)/cmd/zed/zed.d/data-notify.sh \ $(top_srcdir)/cmd/zed/zed.d/generic-notify.sh \ $(top_srcdir)/cmd/zed/zed.d/io-notify.sh \ $(top_srcdir)/cmd/zed/zed.d/io-spare.sh \ $(top_srcdir)/cmd/zed/zed.d/resilver.finish-notify.sh \ $(top_srcdir)/cmd/zed/zed.d/scrub.finish-notify.sh zedconfdefaults = \ all-syslog.sh \ checksum-notify.sh \ checksum-spare.sh \ data-notify.sh \ io-notify.sh \ io-spare.sh \ resilver.finish-notify.sh \ scrub.finish-notify.sh -install-data-local: +install-data-hook: $(MKDIR_P) "$(DESTDIR)$(zedconfdir)" for f in $(zedconfdefaults); do \ test -f "$(DESTDIR)$(zedconfdir)/$${f}" -o \ -L "$(DESTDIR)$(zedconfdir)/$${f}" || \ ln -s "$(zedexecdir)/$${f}" "$(DESTDIR)$(zedconfdir)"; \ done + chmod 0600 "$(DESTDIR)$(zedconfdir)/zed.rc" diff --git a/cmd/zed/zed.d/zed-functions.sh b/cmd/zed/zed.d/zed-functions.sh index 14909d38cb54..cc4f69667ee7 100644 --- a/cmd/zed/zed.d/zed-functions.sh +++ b/cmd/zed/zed.d/zed-functions.sh @@ -1,302 +1,392 @@ # 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)" 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)" 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)) + [ "${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. # # Requires the mail executable to be installed in the standard PATH. # # Arguments # subject: notification subject # pathname: pathname containing the notification message (OPTIONAL) # # Globals # ZED_EMAIL # # Return # 0: notification sent # 1: notification failed # 2: not configured # zed_notify_email() { local subject="$1" local pathname="${2:-"/dev/null"}" [ -n "${ZED_EMAIL}" ] || return 2 [ -n "${subject}" ] || return 1 if [ ! -r "${pathname}" ]; then zed_log_err "mail cannot read \"${pathname}\"" return 1 fi zed_check_cmd "mail" || return 1 mail -s "${subject}" "${ZED_EMAIL}" < "${pathname}" >/dev/null 2>&1; rv=$? if [ "${rv}" -ne 0 ]; then zed_log_err "mail exit=${rv}" return 1 fi return 0 } +# zed_notify_pushbullet (subject, pathname) +# +# Send a notification via Pushbullet . +# 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_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="$(egrep "^[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 egrep -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}" } diff --git a/cmd/zed/zed.d/zed.rc b/cmd/zed/zed.d/zed.rc index 05f84c877304..c2336287f201 100644 --- a/cmd/zed/zed.d/zed.rc +++ b/cmd/zed/zed.d/zed.rc @@ -1,60 +1,80 @@ ## # zed.rc +# +# This file should be owned by root and permissioned 0600. ## ## # Absolute path to the debug output file. # #ZED_DEBUG_LOG="/tmp/zed.debug.log" ## # Email address of the zpool administrator for receipt of notifications. # Email will only be sent if ZED_EMAIL is defined. # Disabled by default; uncomment to enable. # #ZED_EMAIL="root" ## # Default directory for zed lock files. # #ZED_LOCKDIR="/var/lock" ## # Minimum number of seconds between notifications for a similar event. # #ZED_NOTIFY_INTERVAL_SECS=3600 ## # Notification verbosity. # If set to 0, suppress notification if the pool is healthy. # If set to 1, send notification regardless of pool health. # #ZED_NOTIFY_VERBOSE=0 +## +# Pushbullet access token. +# This grants full access to your account -- protect it accordingly! +# +# +# Disabled by default; uncomment to enable. +# +#ZED_PUSHBULLET_ACCESS_TOKEN="" + +## +# Pushbullet channel tag for push notification feeds that can be subscribed to. +# +# If not defined, push notifications will instead be sent to all devices +# associated with the account specified by the access token. +# Disabled by default; uncomment to enable. +# +#ZED_PUSHBULLET_CHANNEL_TAG="" + ## # Default directory for zed state files. # #ZED_RUNDIR="/var/run" ## # Replace a device with a hot spare after N checksum errors are detected. # Disabled by default; uncomment to enable. # #ZED_SPARE_ON_CHECKSUM_ERRORS=10 ## # Replace a device with a hot spare after N I/O errors are detected. # Disabled by default; uncomment to enable. # #ZED_SPARE_ON_IO_ERRORS=1 ## # The syslog priority (e.g., specified as a "facility.level" pair). # #ZED_SYSLOG_PRIORITY="daemon.notice" ## # The syslog tag for marking zed events. # #ZED_SYSLOG_TAG="zed"