]>
Commit | Line | Data |
---|---|---|
aded9a68 CD |
1 | # zed-functions.sh |
2 | # | |
3 | # ZED helper functions for use in ZEDLETs | |
4 | ||
5 | ||
6 | # Variable Defaults | |
7 | # | |
aded9a68 | 8 | : "${ZED_LOCKDIR:="/var/lock"}" |
20967ff1 CD |
9 | : "${ZED_NOTIFY_INTERVAL_SECS:=3600}" |
10 | : "${ZED_NOTIFY_VERBOSE:=0}" | |
aded9a68 CD |
11 | : "${ZED_RUNDIR:="/var/run"}" |
12 | : "${ZED_SYSLOG_PRIORITY:="daemon.notice"}" | |
13 | : "${ZED_SYSLOG_TAG:="zed"}" | |
14 | ||
15 | ZED_FLOCK_FD=8 | |
16 | ||
17 | ||
18 | # zed_check_cmd (cmd, ...) | |
19 | # | |
20 | # For each argument given, search PATH for the executable command [cmd]. | |
21 | # Log a message if [cmd] is not found. | |
22 | # | |
23 | # Arguments | |
24 | # cmd: name of executable command for which to search | |
25 | # | |
26 | # Return | |
27 | # 0 if all commands are found in PATH and are executable | |
28 | # n for a count of the command executables that are not found | |
29 | # | |
30 | zed_check_cmd() | |
31 | { | |
32 | local cmd | |
33 | local rv=0 | |
34 | ||
35 | for cmd; do | |
36 | if ! command -v "${cmd}" >/dev/null 2>&1; then | |
37 | zed_log_err "\"${cmd}\" not installed" | |
38 | rv=$((rv + 1)) | |
39 | fi | |
40 | done | |
41 | return "${rv}" | |
42 | } | |
43 | ||
44 | ||
45 | # zed_log_msg (msg, ...) | |
46 | # | |
47 | # Write all argument strings to the system log. | |
48 | # | |
49 | # Globals | |
50 | # ZED_SYSLOG_PRIORITY | |
51 | # ZED_SYSLOG_TAG | |
52 | # | |
53 | # Return | |
54 | # nothing | |
55 | # | |
56 | zed_log_msg() | |
57 | { | |
58 | logger -p "${ZED_SYSLOG_PRIORITY}" -t "${ZED_SYSLOG_TAG}" -- "$@" | |
59 | } | |
60 | ||
61 | ||
62 | # zed_log_err (msg, ...) | |
63 | # | |
64 | # Write an error message to the system log. This message will contain the | |
65 | # script name, EID, and all argument strings. | |
66 | # | |
67 | # Globals | |
68 | # ZED_SYSLOG_PRIORITY | |
69 | # ZED_SYSLOG_TAG | |
70 | # ZEVENT_EID | |
71 | # | |
72 | # Return | |
73 | # nothing | |
74 | # | |
75 | zed_log_err() | |
76 | { | |
77 | logger -p "${ZED_SYSLOG_PRIORITY}" -t "${ZED_SYSLOG_TAG}" -- "error:" \ | |
78 | "$(basename -- "$0"):" "${ZEVENT_EID:+"eid=${ZEVENT_EID}:"}" "$@" | |
79 | } | |
80 | ||
81 | ||
82 | # zed_lock (lockfile, [fd]) | |
83 | # | |
84 | # Obtain an exclusive (write) lock on [lockfile]. If the lock cannot be | |
85 | # immediately acquired, wait until it becomes available. | |
86 | # | |
87 | # Every zed_lock() must be paired with a corresponding zed_unlock(). | |
88 | # | |
89 | # By default, flock-style locks associate the lockfile with file descriptor 8. | |
90 | # The bash manpage warns that file descriptors >9 should be used with care as | |
91 | # they may conflict with file descriptors used internally by the shell. File | |
92 | # descriptor 9 is reserved for zed_rate_limit(). If concurrent locks are held | |
93 | # within the same process, they must use different file descriptors (preferably | |
94 | # decrementing from 8); otherwise, obtaining a new lock with a given file | |
95 | # descriptor will release the previous lock associated with that descriptor. | |
96 | # | |
97 | # Arguments | |
98 | # lockfile: pathname of the lock file; the lock will be stored in | |
99 | # ZED_LOCKDIR unless the pathname contains a "/". | |
100 | # fd: integer for the file descriptor used by flock (OPTIONAL unless holding | |
101 | # concurrent locks) | |
102 | # | |
103 | # Globals | |
104 | # ZED_FLOCK_FD | |
105 | # ZED_LOCKDIR | |
106 | # | |
107 | # Return | |
108 | # nothing | |
109 | # | |
110 | zed_lock() | |
111 | { | |
112 | local lockfile="$1" | |
113 | local fd="${2:-${ZED_FLOCK_FD}}" | |
114 | local umask_bak | |
115 | local err | |
116 | ||
117 | [ -n "${lockfile}" ] || return | |
118 | if ! expr "${lockfile}" : '.*/' >/dev/null 2>&1; then | |
119 | lockfile="${ZED_LOCKDIR}/${lockfile}" | |
120 | fi | |
121 | ||
122 | umask_bak="$(umask)" | |
123 | umask 077 | |
124 | ||
125 | # Obtain a lock on the file bound to the given file descriptor. | |
126 | # | |
127 | eval "exec ${fd}> '${lockfile}'" | |
128 | err="$(flock --exclusive "${fd}" 2>&1)" | |
129 | if [ $? -ne 0 ]; 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 | # | |
153 | zed_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 | # | |
166 | err="$(flock --unlock "${fd}" 2>&1)" | |
167 | if [ $? -ne 0 ]; then | |
168 | zed_log_err "failed to unlock \"${lockfile}\": ${err}" | |
169 | fi | |
170 | eval "exec ${fd}>&-" | |
171 | } | |
172 | ||
173 | ||
20967ff1 CD |
174 | # zed_notify (subject, pathname) |
175 | # | |
176 | # Send a notification via all available methods. | |
177 | # | |
178 | # Arguments | |
179 | # subject: notification subject | |
180 | # pathname: pathname containing the notification message (OPTIONAL) | |
181 | # | |
182 | # Return | |
183 | # 0: notification succeeded via at least one method | |
184 | # 1: notification failed | |
185 | # 2: no notification methods configured | |
186 | # | |
187 | zed_notify() | |
188 | { | |
189 | local subject="$1" | |
190 | local pathname="$2" | |
191 | local num_success=0 | |
192 | local num_failure=0 | |
193 | ||
194 | zed_notify_email "${subject}" "${pathname}"; rv=$? | |
195 | [ "${rv}" -eq 0 ] && num_success=$((num_success + 1)) | |
196 | [ "${rv}" -eq 1 ] && num_failure=$((num_failure + 1)) | |
197 | ||
a0d065fa CD |
198 | zed_notify_pushbullet "${subject}" "${pathname}"; rv=$? |
199 | [ "${rv}" -eq 0 ] && num_success=$((num_success + 1)) | |
200 | [ "${rv}" -eq 1 ] && num_failure=$((num_failure + 1)) | |
201 | ||
20967ff1 CD |
202 | [ "${num_success}" -gt 0 ] && return 0 |
203 | [ "${num_failure}" -gt 0 ] && return 1 | |
204 | return 2 | |
205 | } | |
206 | ||
207 | ||
208 | # zed_notify_email (subject, pathname) | |
209 | # | |
210 | # Send a notification via email to the address specified by ZED_EMAIL. | |
211 | # | |
212 | # Requires the mail executable to be installed in the standard PATH. | |
213 | # | |
214 | # Arguments | |
215 | # subject: notification subject | |
216 | # pathname: pathname containing the notification message (OPTIONAL) | |
217 | # | |
218 | # Globals | |
219 | # ZED_EMAIL | |
220 | # | |
221 | # Return | |
222 | # 0: notification sent | |
223 | # 1: notification failed | |
224 | # 2: not configured | |
225 | # | |
226 | zed_notify_email() | |
227 | { | |
228 | local subject="$1" | |
229 | local pathname="${2:-"/dev/null"}" | |
230 | ||
231 | [ -n "${ZED_EMAIL}" ] || return 2 | |
232 | ||
233 | [ -n "${subject}" ] || return 1 | |
234 | if [ ! -r "${pathname}" ]; then | |
235 | zed_log_err "mail cannot read \"${pathname}\"" | |
236 | return 1 | |
237 | fi | |
238 | ||
239 | zed_check_cmd "mail" || return 1 | |
240 | ||
241 | mail -s "${subject}" "${ZED_EMAIL}" < "${pathname}" >/dev/null 2>&1; rv=$? | |
242 | if [ "${rv}" -ne 0 ]; then | |
243 | zed_log_err "mail exit=${rv}" | |
244 | return 1 | |
245 | fi | |
246 | return 0 | |
247 | } | |
248 | ||
249 | ||
a0d065fa CD |
250 | # zed_notify_pushbullet (subject, pathname) |
251 | # | |
252 | # Send a notification via Pushbullet <https://www.pushbullet.com/>. | |
253 | # The access token (ZED_PUSHBULLET_ACCESS_TOKEN) identifies this client to the | |
254 | # Pushbullet server. The optional channel tag (ZED_PUSHBULLET_CHANNEL_TAG) is | |
255 | # for pushing to notification feeds that can be subscribed to; if a channel is | |
256 | # not defined, push notifications will instead be sent to all devices | |
257 | # associated with the account specified by the access token. | |
258 | # | |
259 | # Requires awk, curl, and sed executables to be installed in the standard PATH. | |
260 | # | |
261 | # References | |
262 | # https://docs.pushbullet.com/ | |
263 | # https://www.pushbullet.com/security | |
264 | # | |
265 | # Arguments | |
266 | # subject: notification subject | |
267 | # pathname: pathname containing the notification message (OPTIONAL) | |
268 | # | |
269 | # Globals | |
270 | # ZED_PUSHBULLET_ACCESS_TOKEN | |
271 | # ZED_PUSHBULLET_CHANNEL_TAG | |
272 | # | |
273 | # Return | |
274 | # 0: notification sent | |
275 | # 1: notification failed | |
276 | # 2: not configured | |
277 | # | |
278 | zed_notify_pushbullet() | |
279 | { | |
280 | local subject="$1" | |
281 | local pathname="${2:-"/dev/null"}" | |
282 | local msg_body | |
283 | local msg_tag | |
284 | local msg_json | |
285 | local msg_out | |
286 | local msg_err | |
287 | local url="https://api.pushbullet.com/v2/pushes" | |
288 | ||
289 | [ -n "${ZED_PUSHBULLET_ACCESS_TOKEN}" ] || return 2 | |
290 | ||
291 | [ -n "${subject}" ] || return 1 | |
292 | if [ ! -r "${pathname}" ]; then | |
293 | zed_log_err "pushbullet cannot read \"${pathname}\"" | |
294 | return 1 | |
295 | fi | |
296 | ||
297 | zed_check_cmd "awk" "curl" "sed" || return 1 | |
298 | ||
299 | # Escape the following characters in the message body for JSON: | |
300 | # newline, backslash, double quote, horizontal tab, vertical tab, | |
301 | # and carriage return. | |
302 | # | |
303 | msg_body="$(awk '{ ORS="\\n" } { gsub(/\\/, "\\\\"); gsub(/"/, "\\\""); | |
304 | gsub(/\t/, "\\t"); gsub(/\f/, "\\f"); gsub(/\r/, "\\r"); print }' \ | |
305 | "${pathname}")" | |
306 | ||
307 | # Push to a channel if one is configured. | |
308 | # | |
309 | [ -n "${ZED_PUSHBULLET_CHANNEL_TAG}" ] && msg_tag="$(printf \ | |
310 | '"channel_tag": "%s", ' "${ZED_PUSHBULLET_CHANNEL_TAG}")" | |
311 | ||
312 | # Construct the JSON message for pushing a note. | |
313 | # | |
314 | msg_json="$(printf '{%s"type": "note", "title": "%s", "body": "%s"}' \ | |
315 | "${msg_tag}" "${subject}" "${msg_body}")" | |
316 | ||
317 | # Send the POST request and check for errors. | |
318 | # | |
319 | msg_out="$(curl -u "${ZED_PUSHBULLET_ACCESS_TOKEN}:" -X POST "${url}" \ | |
320 | --header "Content-Type: application/json" --data-binary "${msg_json}" \ | |
321 | 2>/dev/null)"; rv=$? | |
322 | if [ "${rv}" -ne 0 ]; then | |
323 | zed_log_err "curl exit=${rv}" | |
324 | return 1 | |
325 | fi | |
326 | msg_err="$(echo "${msg_out}" \ | |
327 | | sed -n -e 's/.*"error" *:.*"message" *: *"\([^"]*\)".*/\1/p')" | |
328 | if [ -n "${msg_err}" ]; then | |
329 | zed_log_err "pushbullet \"${msg_err}"\" | |
330 | return 1 | |
331 | fi | |
332 | return 0 | |
333 | } | |
334 | ||
335 | ||
aded9a68 CD |
336 | # zed_rate_limit (tag, [interval]) |
337 | # | |
338 | # Check whether an event of a given type [tag] has already occurred within the | |
339 | # last [interval] seconds. | |
340 | # | |
341 | # This function obtains a lock on the statefile using file descriptor 9. | |
342 | # | |
343 | # Arguments | |
344 | # tag: arbitrary string for grouping related events to rate-limit | |
345 | # interval: time interval in seconds (OPTIONAL) | |
346 | # | |
347 | # Globals | |
20967ff1 | 348 | # ZED_NOTIFY_INTERVAL_SECS |
aded9a68 CD |
349 | # ZED_RUNDIR |
350 | # | |
351 | # Return | |
352 | # 0 if the event should be processed | |
353 | # 1 if the event should be dropped | |
354 | # | |
355 | # State File Format | |
356 | # time;tag | |
357 | # | |
358 | zed_rate_limit() | |
359 | { | |
360 | local tag="$1" | |
20967ff1 | 361 | local interval="${2:-${ZED_NOTIFY_INTERVAL_SECS}}" |
aded9a68 CD |
362 | local lockfile="zed.zedlet.state.lock" |
363 | local lockfile_fd=9 | |
364 | local statefile="${ZED_RUNDIR}/zed.zedlet.state" | |
365 | local time_now | |
366 | local time_prev | |
367 | local umask_bak | |
368 | local rv=0 | |
369 | ||
370 | [ -n "${tag}" ] || return 0 | |
371 | ||
372 | zed_lock "${lockfile}" "${lockfile_fd}" | |
373 | time_now="$(date +%s)" | |
374 | time_prev="$(egrep "^[0-9]+;${tag}\$" "${statefile}" 2>/dev/null \ | |
375 | | tail -1 | cut -d\; -f1)" | |
376 | ||
377 | if [ -n "${time_prev}" ] \ | |
378 | && [ "$((time_now - time_prev))" -lt "${interval}" ]; then | |
379 | rv=1 | |
380 | else | |
381 | umask_bak="$(umask)" | |
382 | umask 077 | |
383 | egrep -v "^[0-9]+;${tag}\$" "${statefile}" 2>/dev/null \ | |
384 | > "${statefile}.$$" | |
385 | echo "${time_now};${tag}" >> "${statefile}.$$" | |
386 | mv -f "${statefile}.$$" "${statefile}" | |
387 | umask "${umask_bak}" | |
388 | fi | |
389 | ||
390 | zed_unlock "${lockfile}" "${lockfile_fd}" | |
391 | return "${rv}" | |
392 | } |