Licenses: Updated the list of licenses and added a PDF containing all license texts
[check_mk.git] / cmk_base / notify.py
blob91309f088daea59099d50dac1706818fe878d6f0
1 #!/usr/bin/python
2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
9 # | |
10 # | Copyright Mathias Kettner 2014 mk@mathias-kettner.de |
11 # +------------------------------------------------------------------+
13 # This file is part of Check_MK.
14 # The official homepage is at http://mathias-kettner.de/check_mk.
16 # check_mk is free software; you can redistribute it and/or modify it
17 # under the terms of the GNU General Public License as published by
18 # the Free Software Foundation in version 2. check_mk is distributed
19 # in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
20 # out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
21 # PARTICULAR PURPOSE. See the GNU General Public License for more de-
22 # tails. You should have received a copy of the GNU General Public
23 # License along with GNU Make; see the file COPYING. If not, write
24 # to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
25 # Boston, MA 02110-1301 USA.
27 # Please have a look at doc/Notifications.png:
29 # There are two types of contexts:
30 # 1. Raw contexts (purple)
31 # => These come right out of the monitoring core. They are not yet
32 # assinged to a certain plugin. In case of rule based notifictions
33 # they are not even assigned to a certain contact.
35 # 2. Plugin contexts (cyan)
36 # => These already bear all information about the contact, the plugin
37 # to call and its parameters.
39 import os
40 import pprint
41 import re
42 import signal
43 import subprocess
44 import sys
45 import time
47 # suppress "Cannot find module" error from mypy
48 import livestatus # type: ignore
49 import cmk.utils.debug
50 from cmk.utils.notify import notification_message
51 from cmk.utils.regex import regex
52 import cmk.utils.paths
53 from cmk.utils.exceptions import MKGeneralException
55 import cmk_base.utils
56 import cmk_base.config as config
57 import cmk_base.console as console
58 import cmk_base.core
59 import cmk_base.events as events
61 try:
62 import cmk_base.cee.keepalive as keepalive
63 except ImportError:
64 keepalive = None # type: ignore
66 _log_to_stdout = False
67 notify_mode = "notify"
69 # .--Configuration-------------------------------------------------------.
70 # | ____ __ _ _ _ |
71 # | / ___|___ _ __ / _(_) __ _ _ _ _ __ __ _| |_(_) ___ _ __ |
72 # | | | / _ \| '_ \| |_| |/ _` | | | | '__/ _` | __| |/ _ \| '_ \ |
73 # | | |__| (_) | | | | _| | (_| | |_| | | | (_| | |_| | (_) | | | | |
74 # | \____\___/|_| |_|_| |_|\__, |\__,_|_| \__,_|\__|_|\___/|_| |_| |
75 # | |___/ |
76 # +----------------------------------------------------------------------+
77 # | Default values of global configuration variables. |
78 # '----------------------------------------------------------------------'
80 # Default settings
81 notification_logdir = cmk.utils.paths.var_dir + "/notify"
82 notification_spooldir = cmk.utils.paths.var_dir + "/notify/spool"
83 notification_bulkdir = cmk.utils.paths.var_dir + "/notify/bulk"
84 notification_core_log = cmk.utils.paths.var_dir + "/notify/nagios.log" # Fallback for history if no CMC running
85 notification_log = cmk.utils.paths.log_dir + "/notify.log"
87 notification_log_template = \
88 u"$CONTACTNAME$ - $NOTIFICATIONTYPE$ - " \
89 u"$HOSTNAME$ $HOSTSTATE$ - " \
90 u"$SERVICEDESC$ $SERVICESTATE$ "
92 notification_mail_command = u"mail -s '$SUBJECT$' '$CONTACTEMAIL$'"
93 notification_host_subject = u"Check_MK: $HOSTNAME$ - $NOTIFICATIONTYPE$"
94 notification_service_subject = u"Check_MK: $HOSTNAME$/$SERVICEDESC$ $NOTIFICATIONTYPE$"
96 notification_common_body = u"""Host: $HOSTNAME$
97 Alias: $HOSTALIAS$
98 Address: $HOSTADDRESS$
99 """
101 notification_host_body = u"""State: $LASTHOSTSTATE$ -> $HOSTSTATE$ ($NOTIFICATIONTYPE$)
102 Command: $HOSTCHECKCOMMAND$
103 Output: $HOSTOUTPUT$
104 Perfdata: $HOSTPERFDATA$
105 $LONGHOSTOUTPUT$
108 notification_service_body = u"""Service: $SERVICEDESC$
109 State: $LASTSERVICESTATE$ -> $SERVICESTATE$ ($NOTIFICATIONTYPE$)
110 Command: $SERVICECHECKCOMMAND$
111 Output: $SERVICEOUTPUT$
112 Perfdata: $SERVICEPERFDATA$
113 $LONGSERVICEOUTPUT$
117 # .--helper--------------------------------------------------------------.
118 # | _ _ |
119 # | | |__ ___| |_ __ ___ _ __ |
120 # | | '_ \ / _ \ | '_ \ / _ \ '__| |
121 # | | | | | __/ | |_) | __/ | |
122 # | |_| |_|\___|_| .__/ \___|_| |
123 # | |_| |
124 # '----------------------------------------------------------------------'
127 def _transform_user_disable_notifications_opts(contact):
128 if "disable_notifications" in contact and isinstance(contact["disable_notifications"], bool):
129 return {"disable": contact["disable_notifications"]}
130 return contact.get("disable_notifications", {})
134 # .--Main----------------------------------------------------------------.
135 # | __ __ _ |
136 # | | \/ | __ _(_)_ __ |
137 # | | |\/| |/ _` | | '_ \ |
138 # | | | | | (_| | | | | | |
139 # | |_| |_|\__,_|_|_| |_| |
140 # | |
141 # +----------------------------------------------------------------------+
142 # | Main code entry point. |
143 # '----------------------------------------------------------------------'
146 def notify_usage():
147 console.error("""Usage: check_mk --notify [--keepalive]
148 check_mk --notify spoolfile <filename>
150 Normally the notify module is called without arguments to send real
151 notification. But there are situations where this module is called with
152 COMMANDS to e.g. support development of notification plugins.
154 Available commands:
155 spoolfile <filename> Reads the given spoolfile and creates a
156 notification out of its data
157 stdin Read one notification context from stdin instead
158 of taking variables from environment
159 replay N Uses the N'th recent notification from the backlog
160 and sends it again, counting from 0.
161 send-bulks Send out ripe bulk notifications
162 """)
165 # Main function called by cmk --notify. It either starts the
166 # keepalive mode (used by CMC), sends out one notifications from
167 # several possible sources or sends out all ripe bulk notifications.
168 def do_notify(options, args):
169 global _log_to_stdout, notify_mode
170 _log_to_stdout = options.get("log-to-stdout", _log_to_stdout)
172 if keepalive and "keepalive" in options:
173 keepalive.enable()
175 convert_legacy_configuration()
177 try:
178 if not os.path.exists(notification_logdir):
179 os.makedirs(notification_logdir)
180 if not os.path.exists(notification_spooldir):
181 os.makedirs(notification_spooldir)
183 notify_mode = 'notify'
184 if args:
185 notify_mode = args[0]
186 if notify_mode not in ['stdin', 'spoolfile', 'replay', 'send-bulks']:
187 console.error("ERROR: Invalid call to check_mk --notify.\n\n")
188 notify_usage()
189 sys.exit(1)
191 if len(args) != 2 and notify_mode not in ["stdin", "replay", "send-bulks"]:
192 console.error("ERROR: need an argument to --notify %s.\n\n" % notify_mode)
193 sys.exit(1)
195 elif notify_mode == 'spoolfile':
196 filename = args[1]
198 elif notify_mode == 'replay':
199 try:
200 replay_nr = int(args[1])
201 except:
202 replay_nr = 0
204 # If the notify_mode is set to 'spoolfile' we try to parse the given spoolfile
205 # This spoolfile contains a python dictionary
206 # { context: { Dictionary of environment variables }, plugin: "Plugin name" }
207 # Any problems while reading the spoolfile results in returning 2
208 # -> mknotifyd deletes this file
209 if notify_mode == "spoolfile":
210 return handle_spoolfile(filename)
212 elif keepalive and keepalive.enabled():
213 notify_keepalive()
215 elif notify_mode == 'replay':
216 raw_context = raw_context_from_backlog(replay_nr)
217 notify_notify(raw_context)
219 elif notify_mode == 'stdin':
220 notify_notify(events.raw_context_from_stdin())
222 elif notify_mode == "send-bulks":
223 send_ripe_bulks()
225 else:
226 notify_notify(raw_context_from_env())
228 except Exception:
229 crash_dir = cmk.utils.paths.var_dir + "/notify"
230 if not os.path.exists(crash_dir):
231 os.makedirs(crash_dir)
232 file(crash_dir + "/crash.log", "a").write(
233 "CRASH (%s):\n%s\n" % (time.strftime("%Y-%m-%d %H:%M:%S"), format_exception()))
236 def convert_legacy_configuration():
237 # Convert legacy spooling configuration to new one (see above)
238 if config.notification_spooling in (True, False):
239 if config.notification_spool_to:
240 also_local = config.notification_spool_to[2]
241 if also_local:
242 config.notification_spooling = "both"
243 else:
244 config.notification_spooling = "remote"
245 elif config.notification_spooling:
246 config.notification_spooling = "local"
247 else:
248 config.notification_spooling = "remote"
250 # The former values 1 and 2 are mapped to the values 20 (default) and 10 (debug)
251 # which agree with the values used in cmk/utils/log.py.
252 # The decprecated value 0 is transformed to the default logging value.
253 if config.notification_logging in [0, 1]:
254 config.notification_logging = 20
255 elif config.notification_logging == 2:
256 config.notification_logging = 10
259 # This function processes one raw notification and decides wether it
260 # should be spooled or not. In the latter cased a local delivery
261 # is being done.
262 def notify_notify(raw_context, analyse=False):
263 if not analyse:
264 store_notification_backlog(raw_context)
266 notify_log("----------------------------------------------------------------------")
267 if analyse:
268 notify_log("Analysing notification (%s) context with %s variables" %
269 (events.find_host_service_in_context(raw_context), len(raw_context)))
270 else:
271 notify_log("Got raw notification (%s) context with %s variables" %
272 (events.find_host_service_in_context(raw_context), len(raw_context)))
274 # Add some further variable for the conveniance of the plugins
276 notify_log_debug(events.render_context_dump(raw_context))
278 _complete_raw_context_with_notification_vars(raw_context)
279 events.complete_raw_context(
280 raw_context, with_dump=config.notification_logging <= 10, log_func=notify_log)
282 # Spool notification to remote host, if this is enabled
283 if config.notification_spooling in ("remote", "both"):
284 create_spoolfile({"context": raw_context, "forward": True})
286 if config.notification_spooling != "remote":
287 return locally_deliver_raw_context(raw_context, analyse=analyse)
290 # Add some notification specific variables to the context. These are currently
291 # not added to alert handler scripts
292 def _complete_raw_context_with_notification_vars(raw_context):
293 raw_context["LOGDIR"] = notification_logdir
294 raw_context["MAIL_COMMAND"] = notification_mail_command
297 # Here we decide which notification implementation we are using.
298 # Hopefully we can drop a couple of them some day
299 # 1. Rule Based Notifiations (since 1.2.5i1)
300 # 2. Flexible Notifications (since 1.2.2)
301 # 3. Plain email notification (refer to git log if you are really interested)
302 def locally_deliver_raw_context(raw_context, analyse=False):
303 contactname = raw_context.get("CONTACTNAME")
304 try:
306 # If rule based notifications are enabled then the Micro Core does not set the
307 # variable CONTACTNAME. In the other cores the CONTACTNAME is being set to
308 # check-mk-notify.
309 # We do we not simply check the config variable enable_rulebased_notifications?
310 # -> Because the core needs are restart in order to reflect this while the
311 # notification mode of Check_MK not. There are thus situations where the
312 # setting of the core is different from our global variable. The core must
313 # have precedence in this situation!
314 if not contactname or contactname == "check-mk-notify":
315 # 1. RULE BASE NOTIFICATIONS
316 notify_log_debug("Preparing rule based notifications")
317 return notify_rulebased(raw_context, analyse=analyse)
319 if analyse:
320 return # Analysis only possible when rule based notifications are enabled
322 # Now fetch all configuration about that contact (it needs to be configure via
323 # Check_MK for that purpose). If we do not know that contact then we cannot use
324 # flexible notifications even if they are enabled.
325 contact = config.contacts.get(contactname)
327 disable_notifications_opts = _transform_user_disable_notifications_opts(contact)
328 if disable_notifications_opts.get("disable", False):
329 start, end = disable_notifications_opts.get("timerange", (None, None))
330 if start is None or end is None:
331 notify_log("Notifications for %s are disabled in personal settings. Skipping." %
332 contactname)
333 return
334 elif start <= time.time() <= end:
335 notify_log(
336 "Notifications for %s are disabled in personal settings from %s to %s. Skipping."
337 % (contactname, start, end))
338 return
340 # Get notification settings for the contact in question - if available.
341 if contact:
342 method = contact.get("notification_method", "email")
343 else:
344 method = "email"
346 if isinstance(method, tuple) and method[0] == 'flexible':
347 # 2. FLEXIBLE NOTIFICATIONS
348 notify_log("Preparing flexible notifications for %s" % contactname)
349 notify_flexible(raw_context, method[1])
351 else:
352 # 3. PLAIN EMAIL NOTIFICATION
353 notify_log("Preparing plain email notifications for %s" % contactname)
354 notify_plain_email(raw_context)
356 except Exception as e:
357 if cmk.utils.debug.enabled():
358 raise
359 notify_log("ERROR: %s\n%s" % (e, format_exception()))
362 def notification_replay_backlog(nr):
363 global notify_mode
364 notify_mode = "replay"
365 raw_context = raw_context_from_backlog(nr)
366 notify_notify(raw_context)
369 def notification_analyse_backlog(nr):
370 global notify_mode
371 notify_mode = "replay"
372 raw_context = raw_context_from_backlog(nr)
373 return notify_notify(raw_context, analyse=True)
377 # .--Keepalive-Mode (Used by CMC)----------------------------------------.
378 # | _ __ _ _ |
379 # | | |/ /___ ___ _ __ __ _| (_)_ _____ |
380 # | | ' // _ \/ _ \ '_ \ / _` | | \ \ / / _ \ |
381 # | | . \ __/ __/ |_) | (_| | | |\ V / __/ |
382 # | |_|\_\___|\___| .__/ \__,_|_|_| \_/ \___| |
383 # | |_| |
384 # +----------------------------------------------------------------------+
385 # | Implementation of cmk --notify --keepalive, which is being used |
386 # | by the Micro Core. |
387 # '----------------------------------------------------------------------'
390 # TODO: Make use of the generic do_keepalive() mechanism?
391 def notify_keepalive():
392 cmk_base.utils.register_sigint_handler()
393 events.event_keepalive(
394 event_function=notify_notify,
395 log_function=notify_log,
396 call_every_loop=send_ripe_bulks,
397 loop_interval=config.notification_bulk_interval,
402 # .--Rule-Based-Notifications--------------------------------------------.
403 # | ____ _ _ _ |
404 # | | _ \ _ _| | ___| |__ __ _ ___ ___ __| | |
405 # | | |_) | | | | |/ _ \ '_ \ / _` / __|/ _ \/ _` | |
406 # | | _ <| |_| | | __/ |_) | (_| \__ \ __/ (_| | |
407 # | |_| \_\\__,_|_|\___|_.__/ \__,_|___/\___|\__,_| |
408 # | |
409 # +----------------------------------------------------------------------+
410 # | Logic for rule based notifications |
411 # '----------------------------------------------------------------------'
414 def notify_rulebased(raw_context, analyse=False):
415 # First step: go through all rules and construct our table of
416 # notification plugins to call. This is a dict from (users, plugin) to
417 # a triple of (locked, parameters, bulk). If locked is True, then a user
418 # cannot cancel this notification via his personal notification rules.
419 # Example:
420 # notifications = {
421 # ( frozenset({"aa", "hh", "ll"}), "email" ) : ( False, [], None ),
422 # ( frozenset({"hh"}), "sms" ) : ( True, [ "0171737337", "bar", {
423 # 'groupby': 'host', 'interval': 60} ] ),
426 notifications = {}
427 num_rule_matches = 0
428 rule_info = []
430 for rule in config.notification_rules + user_notification_rules():
431 if "contact" in rule:
432 contact_info = "User %s's rule '%s'..." % (rule["contact"], rule["description"])
433 else:
434 contact_info = "Global rule '%s'..." % rule["description"]
436 why_not = rbn_match_rule(rule, raw_context) # also checks disabling
437 if why_not:
438 notify_log_verbose(contact_info)
439 notify_log_verbose(" -> does not match: %s" % why_not)
440 rule_info.append(("miss", rule, why_not))
441 else:
442 notify_log(contact_info)
443 notify_log(" -> matches!")
444 num_rule_matches += 1
445 contacts = rbn_rule_contacts(rule, raw_context)
446 contactstxt = ", ".join(contacts)
448 # Handle old-style and new-style rules
449 if "notify_method" in rule: # old-style
450 plugin = rule["notify_plugin"]
451 plugin_parameters = rule[
452 "notify_method"] # None: do cancel, [ str ]: plugin parameters
453 else:
454 plugin, plugin_parameters = rule["notify_plugin"]
455 plugintxt = plugin or "plain email"
457 key = contacts, plugin
458 if plugin_parameters is None: # cancelling
459 # FIXME: In Python 2, notifications.keys() already produces a
460 # copy of the keys, while in Python 3 it is only a view of the
461 # underlying dict (modifications would result in an exception).
462 # To be explicit and future-proof, we make this hack explicit.
463 # Anyway, this is extremely ugly and an anti-patter, and it
464 # should be rewritten to something more sane.
465 for notify_key in list(notifications.keys()):
466 notify_contacts, notify_plugin = notify_key
468 overlap = notify_contacts.intersection(contacts)
469 if plugin != notify_plugin or not overlap:
470 continue
472 locked, plugin_parameters, bulk = notifications[notify_key]
474 if locked and "contact" in rule:
475 notify_log(" - cannot cancel notification of %s via %s: it is locked" %
476 (contactstxt, plugintxt))
477 continue
479 notify_log(" - cancelling notification of %s via %s" % (", ".join(overlap),
480 plugintxt))
482 remaining = notify_contacts.difference(contacts)
483 if not remaining:
484 del notifications[notify_key]
485 else:
486 new_key = remaining, plugin
487 notifications[new_key] = notifications.pop(notify_key)
488 elif contacts:
489 if key in notifications:
490 locked = notifications[key][0]
491 if locked and "contact" in rule:
492 notify_log(" - cannot modify notification of %s via %s: it is locked" %
493 (contactstxt, plugintxt))
494 continue
495 notify_log(
496 " - modifying notification of %s via %s" % (contactstxt, plugintxt))
497 else:
498 notify_log(" - adding notification of %s via %s" % (contactstxt, plugintxt))
499 bulk = rbn_get_bulk_params(rule)
500 final_parameters = rbn_finalize_plugin_parameters(raw_context["HOSTNAME"], plugin,
501 plugin_parameters)
502 notifications[key] = (not rule.get("allow_disable"), final_parameters, bulk)
504 rule_info.append(("match", rule, ""))
506 plugin_info = []
508 if not notifications:
509 if num_rule_matches:
510 notify_log("%d rules matched, but no notification has been created." % num_rule_matches)
511 elif not analyse:
512 fallback_contacts = rbn_fallback_contacts()
513 if fallback_contacts:
514 notify_log("No rule matched, notifying fallback contacts")
515 fallback_emails = [fc["email"] for fc in fallback_contacts]
516 notify_log(" Sending plain email to %s" % fallback_emails)
518 plugin_context = create_plugin_context(raw_context, [])
519 rbn_add_contact_information(plugin_context, fallback_contacts)
520 notify_via_email(plugin_context)
521 else:
522 notify_log("No rule matched, would notify fallback contacts, but none configured")
523 else:
524 # Now do the actual notifications
525 notify_log("Executing %d notifications:" % len(notifications))
526 entries = notifications.items()
527 entries.sort()
528 for (contacts, plugin), (locked, params, bulk) in entries:
529 verb = "would notify" if analyse else "notifying"
530 contactstxt = ", ".join(contacts)
531 plugintxt = plugin or "plain email"
532 paramtxt = ", ".join(params) if params else "(no parameters)"
533 bulktxt = "yes" if bulk else "no"
534 notify_log(" * %s %s via %s, parameters: %s, bulk: %s" % (verb, contactstxt, plugintxt,
535 paramtxt, bulktxt))
537 try:
538 plugin_context = create_plugin_context(raw_context, params)
539 rbn_add_contact_information(plugin_context, contacts)
541 split_contexts = (
542 plugin not in ["", "mail", "asciimail", "slack"] or
543 # params can be a list (e.g. for custom notificatios)
544 params.get("disable_multiplexing") or bulk)
545 if not split_contexts:
546 plugin_contexts = [plugin_context]
547 else:
548 plugin_contexts = rbn_split_plugin_context(plugin_context)
550 for context in plugin_contexts:
551 plugin_info.append((context["CONTACTNAME"], plugin, params, bulk))
553 if analyse:
554 continue
555 elif bulk:
556 do_bulk_notify(plugin, params, context, bulk)
557 elif config.notification_spooling in ("local", "both"):
558 create_spoolfile({"context": context, "plugin": plugin})
559 else:
560 call_notification_script(plugin, context)
562 except Exception as e:
563 if cmk.utils.debug.enabled():
564 raise
565 fe = format_exception()
566 notify_log(" ERROR: %s" % e)
567 notify_log(fe)
569 analysis_info = rule_info, plugin_info
570 return analysis_info
573 def rbn_fallback_contacts():
574 fallback_contacts = []
575 if config.notification_fallback_email:
576 fallback_contacts.append(rbn_fake_email_contact(config.notification_fallback_email))
578 for contact_name, contact in config.contacts.items():
579 if contact.get("fallback_contact", False) and contact.get("email"):
580 fallback_contact = {
581 "name": contact_name,
583 fallback_contact.update(contact)
584 fallback_contacts.append(fallback_contact)
586 return fallback_contacts
589 def rbn_finalize_plugin_parameters(hostname, plugin, rule_parameters):
590 # Right now we are only able to finalize notification plugins with dict parameters..
591 if isinstance(rule_parameters, dict):
592 parameters = config.host_extra_conf_merged(hostname,
593 config.notification_parameters.get(plugin, []))
594 parameters.update(rule_parameters)
595 return parameters
597 return rule_parameters
600 # Create a table of all user specific notification rules. Important:
601 # create deterministic order, so that rule analyses can depend on
602 # rule indices
603 def user_notification_rules():
604 user_rules = []
605 contactnames = config.contacts.keys()
606 contactnames.sort()
607 for contactname in contactnames:
608 contact = config.contacts[contactname]
609 for rule in contact.get("notification_rules", []):
610 # User notification rules always use allow_disable
611 # This line here is for legacy reasons. Newer versions
612 # already set the allow_disable option in the rule configuration
613 rule["allow_disable"] = True
615 # Save the owner of the rule for later debugging
616 rule["contact"] = contactname
617 # We assume that the "contact_..." entries in the
618 # rule are allowed and only contain one entry of the
619 # type "contact_users" : [ contactname ]. This
620 # is handled by WATO. Contact specific rules are a
621 # WATO-only feature anyway...
622 user_rules.append(rule)
624 if "authorized_sites" in config.contacts[contactname] and not "match_site" in rule:
625 rule["match_site"] = config.contacts[contactname]["authorized_sites"]
627 notify_log_debug("Found %d user specific rules" % len(user_rules))
628 return user_rules
631 def rbn_fake_email_contact(email):
632 return {
633 "name": "mailto:" + email,
634 "alias": "Explicit email adress " + email,
635 "email": email,
636 "pager": "",
640 def rbn_add_contact_information(plugin_context, contacts):
641 # TODO tb: Make contacts a reliable type. Righ now contacts can be
642 # a list of dicts or a frozenset of strings.
643 contact_dicts = []
644 keys = {"name", "alias", "email", "pager"}
646 for contact in contacts:
647 if isinstance(contact, dict):
648 contact_dict = contact
649 elif contact.startswith("mailto:"): # Fake contact
650 contact_dict = {
651 "name": contact[7:],
652 "alias": "Email address " + contact,
653 "email": contact[7:],
654 "pager": ""
656 else:
657 contact_dict = config.contacts.get(contact, {"alias": contact})
658 contact_dict["name"] = contact
660 contact_dicts.append(contact_dict)
661 keys.update([key for key in contact_dict.keys() if key.startswith("_")])
663 for key in keys:
664 context_key = "CONTACT" + key.upper()
665 items = [contact.get(key, "") for contact in contact_dicts]
666 plugin_context[context_key] = ",".join(items)
669 def rbn_split_plugin_context(plugin_context):
670 """Takes a plugin_context containing multiple contacts and returns
671 a list of plugin_contexts with a context for each contact"""
672 num_contacts = len(plugin_context["CONTACTNAME"].split(","))
673 if num_contacts <= 1:
674 return [plugin_context]
676 contexts = []
677 keys_to_split = {"CONTACTNAME", "CONTACTALIAS", "CONTACTEMAIL", "CONTACTPAGER"}
678 keys_to_split.update([key for key in plugin_context.keys() if key.startswith("CONTACT_")])
680 for i in range(num_contacts):
681 context = plugin_context.copy()
682 for key in keys_to_split:
683 context[key] = context[key].split(",")[i]
684 contexts.append(context)
686 return contexts
689 def rbn_get_bulk_params(rule):
690 bulk = rule.get("bulk")
692 if not bulk:
693 return None
694 elif isinstance(bulk, dict): # old format: treat as "Always Bulk"
695 method, params = "always", bulk
696 else:
697 method, params = bulk
699 if method == "always":
700 return params
702 elif method == "timeperiod":
703 try:
704 active = cmk_base.core.timeperiod_active(params["timeperiod"])
705 except:
706 if cmk.utils.debug.enabled():
707 raise
708 # If a livestatus connection error appears we will bulk the
709 # notification in the first place. When the connection is available
710 # again and the period is not active the notifications will be sent.
711 notify_log(" - Error checking activity of timeperiod %s: assuming active" %
712 params["timeperiod"])
713 active = True
715 if active:
716 return params
717 return params.get("bulk_outside")
719 notify_log(" - Unknown bulking method: assuming bulking is disabled")
720 return None
723 def rbn_match_rule(rule, context):
724 if rule.get("disabled"):
725 return "This rule is disabled"
727 return (events.event_match_rule(rule, context) or rbn_match_escalation(rule, context) or
728 rbn_match_escalation_throtte(rule, context) or rbn_match_host_event(rule, context) or
729 rbn_match_service_event(rule, context) or
730 rbn_match_notification_comment(rule, context) or rbn_match_event_console(rule, context))
733 def rbn_match_escalation(rule, context):
734 if "match_escalation" in rule:
735 from_number, to_number = rule["match_escalation"]
736 if context["WHAT"] == "HOST":
737 notification_number = int(context.get("HOSTNOTIFICATIONNUMBER", 1))
738 else:
739 notification_number = int(context.get("SERVICENOTIFICATIONNUMBER", 1))
740 if notification_number < from_number or notification_number > to_number:
741 return "The notification number %d does not lie in range %d ... %d" % (
742 notification_number, from_number, to_number)
745 def rbn_match_escalation_throtte(rule, context):
746 if "match_escalation_throttle" in rule:
747 # We do not want to suppress recovery notifications.
748 if (context["WHAT"] == "HOST" and context.get("HOSTSTATE", "UP") == "UP") or \
749 (context["WHAT"] == "SERVICE" and context.get("SERVICESTATE", "OK") == "OK"):
750 return
751 from_number, rate = rule["match_escalation_throttle"]
752 if context["WHAT"] == "HOST":
753 notification_number = int(context.get("HOSTNOTIFICATIONNUMBER", 1))
754 else:
755 notification_number = int(context.get("SERVICENOTIFICATIONNUMBER", 1))
756 if notification_number <= from_number:
757 return
758 if (notification_number - from_number) % rate != 0:
759 return "This notification is being skipped due to throttling. The next number will be %d" % \
760 (notification_number + rate - ((notification_number - from_number) % rate))
763 def rbn_match_host_event(rule, context):
764 if "match_host_event" in rule:
765 if context["WHAT"] != "HOST":
766 if "match_service_event" not in rule:
767 return "This is a service notification, but the rule just matches host events"
768 return # Let this be handled by match_service_event
770 allowed_events = rule["match_host_event"]
771 state = context["HOSTSTATE"]
772 last_state = context["PREVIOUSHOSTHARDSTATE"]
773 event_map = {"UP": 'r', "DOWN": 'd', "UNREACHABLE": 'u'}
774 return rbn_match_event(context, state, last_state, event_map, allowed_events)
777 def rbn_match_service_event(rule, context):
778 if "match_service_event" in rule:
779 if context["WHAT"] != "SERVICE":
780 if "match_host_event" not in rule:
781 return "This is a host notification, but the rule just matches service events"
782 return # Let this be handled by match_host_event
784 allowed_events = rule["match_service_event"]
785 state = context["SERVICESTATE"]
786 last_state = context["PREVIOUSSERVICEHARDSTATE"]
787 event_map = {"OK": 'r', "WARNING": 'w', "CRITICAL": 'c', "UNKNOWN": 'u'}
788 return rbn_match_event(context, state, last_state, event_map, allowed_events)
791 def rbn_match_event(context, state, last_state, event_map, allowed_events):
792 notification_type = context["NOTIFICATIONTYPE"]
794 if notification_type == "RECOVERY":
795 event = event_map.get(last_state, '?') + 'r'
796 elif notification_type in ["FLAPPINGSTART", "FLAPPINGSTOP", "FLAPPINGDISABLED"]:
797 event = 'f'
798 elif notification_type in ["DOWNTIMESTART", "DOWNTIMEEND", "DOWNTIMECANCELLED"]:
799 event = 's'
800 elif notification_type == "ACKNOWLEDGEMENT":
801 event = 'x'
802 elif notification_type.startswith("ALERTHANDLER ("):
803 handler_state = notification_type[14:-1]
804 if handler_state == "OK":
805 event = 'as'
806 else:
807 event = 'af'
808 else:
809 event = event_map.get(last_state, '?') + event_map.get(state, '?')
811 # Now go through the allowed events. Handle '?' has matching all types!
812 for allowed in allowed_events:
813 if event == allowed or \
814 (allowed[0] == '?' and len(event) > 1 and event[1] == allowed[1]) or \
815 (event[0] == '?' and len(allowed) > 1 and event[1] == allowed[1]):
816 return
818 return "Event type '%s' not handled by this rule. Allowed are: %s" % (event,
819 ", ".join(allowed_events))
822 def rbn_rule_contacts(rule, context):
823 the_contacts = set([])
824 if rule.get("contact_object"):
825 the_contacts.update(rbn_object_contact_names(context))
826 if rule.get("contact_all"):
827 the_contacts.update(rbn_all_contacts())
828 if rule.get("contact_all_with_email"):
829 the_contacts.update(rbn_all_contacts(with_email=True))
830 if "contact_users" in rule:
831 the_contacts.update(rule["contact_users"])
832 if "contact_groups" in rule:
833 the_contacts.update(rbn_groups_contacts(rule["contact_groups"]))
834 if "contact_emails" in rule:
835 the_contacts.update(rbn_emails_contacts(rule["contact_emails"]))
837 all_enabled = []
838 for contactname in the_contacts:
839 if contactname == config.notification_fallback_email:
840 contact = rbn_fake_email_contact(config.notification_fallback_email)
841 else:
842 contact = config.contacts.get(contactname)
844 if contact:
845 disable_notifications_opts = _transform_user_disable_notifications_opts(contact)
846 if disable_notifications_opts.get("disable", False):
847 start, end = disable_notifications_opts.get("timerange", (None, None))
848 if start is None or end is None:
849 notify_log(
850 " - skipping contact %s: he/she has disabled notifications" % contactname)
851 continue
852 elif start <= time.time() <= end:
853 notify_log(
854 " - skipping contact %s: he/she has disabled notifications from %s to %s."
855 % (contactname, start, end))
856 continue
858 reason = (rbn_match_contact_macros(rule, contactname, contact) or
859 rbn_match_contact_groups(rule, contactname, contact))
861 if reason:
862 notify_log(" - skipping contact %s: %s" % (contactname, reason))
863 continue
865 else:
866 notify_log("Warning: cannot get information about contact %s: ignoring restrictions" %
867 contactname)
869 all_enabled.append(contactname)
871 return frozenset(all_enabled) # has to be hashable
874 def rbn_match_contact_macros(rule, contactname, contact):
875 if "contact_match_macros" in rule:
876 for macro_name, regexp in rule["contact_match_macros"]:
877 value = contact.get("_" + macro_name, "")
878 if not regexp.endswith("$"):
879 regexp = regexp + "$"
880 if not regex(regexp).match(value):
881 macro_overview = ", ".join([
882 "%s=%s" % (varname[1:], val)
883 for (varname, val) in contact.items()
884 if varname.startswith("_")
886 return "value '%s' for macro '%s' does not match '%s'. His macros are: %s" % (
887 value, macro_name, regexp, macro_overview)
890 def rbn_match_contact_groups(rule, contactname, contact):
891 if "contact_match_groups" in rule:
892 if "contactgroups" not in contact:
893 notify_log("Warning: cannot determine contact groups of %s: skipping restrictions" %
894 contactname)
895 return
896 for required_group in rule["contact_match_groups"]:
897 if required_group not in contact["contactgroups"]:
898 return "he/she is not member of the contact group %s (his groups are %s)" % (
899 required_group, ", ".join(contact["contactgroups"] or ["<None>"]))
902 def rbn_match_notification_comment(rule, context):
903 if "match_notification_comment" in rule:
904 r = regex(rule["match_notification_comment"])
905 notification_comment = context.get("NOTIFICATIONCOMMENT", "")
906 if not r.match(notification_comment):
907 return "The beginning of the notification comment '%s' is not matched by the regex '%s'" % (
908 notification_comment, rule["match_notification_comment"])
911 def rbn_match_event_console(rule, context):
912 if "match_ec" in rule:
913 match_ec = rule["match_ec"]
914 is_ec_notification = "EC_ID" in context
915 if match_ec is False and is_ec_notification:
916 return "Notification has been created by the Event Console."
917 elif match_ec is not False and not is_ec_notification:
918 return "Notification has not been created by the Event Console."
920 if match_ec is not False:
922 # Match Event Console rule ID
923 if "match_rule_id" in match_ec and context["EC_RULE_ID"] not in match_ec[
924 "match_rule_id"]:
925 return "EC Event has rule ID '%s', but '%s' is required" % (
926 context["EC_RULE_ID"], match_ec["match_rule_id"])
928 # Match syslog priority of event
929 if "match_priority" in match_ec:
930 prio_from, prio_to = match_ec["match_priority"]
931 if prio_from > prio_to:
932 prio_to, prio_from = prio_from, prio_to
933 p = int(context["EC_PRIORITY"])
934 if p < prio_from or p > prio_to:
935 return "Event has priority %s, but matched range is %s .. %s" % (
936 p, prio_from, prio_to)
938 # Match syslog facility of event
939 if "match_facility" in match_ec:
940 if match_ec["match_facility"] != int(context["EC_FACILITY"]):
941 return "Wrong syslog facility %s, required is %s" % (context["EC_FACILITY"],
942 match_ec["match_facility"])
944 # Match event comment
945 if "match_comment" in match_ec:
946 r = regex(match_ec["match_comment"])
947 if not r.search(context["EC_COMMENT"]):
948 return "The event comment '%s' does not match the regular expression '%s'" % (
949 context["EC_COMMENT"], match_ec["match_comment"])
952 def rbn_object_contact_names(context):
953 commasepped = context.get("CONTACTS")
954 if commasepped == "?":
955 notify_log("Warning: Contacts of %s cannot be determined. Using fallback contacts" %
956 events.find_host_service_in_context(context))
957 return [contact["name"] for contact in rbn_fallback_contacts()]
958 elif commasepped:
959 return commasepped.split(",")
961 return []
964 def rbn_all_contacts(with_email=None):
965 if not with_email:
966 return config.contacts.keys() # We have that via our main.mk contact definitions!
968 return [contact_id for (contact_id, contact) in config.contacts.items() if contact.get("email")]
971 def rbn_groups_contacts(groups):
972 if not groups:
973 return {}
974 query = "GET contactgroups\nColumns: members\n"
975 for group in groups:
976 query += "Filter: name = %s\n" % group
977 query += "Or: %d\n" % len(groups)
979 try:
980 contacts = set([])
981 for contact_list in livestatus.LocalConnection().query_column(query):
982 contacts.update(contact_list)
983 return contacts
985 except livestatus.MKLivestatusNotFoundError:
986 return []
988 except Exception:
989 if cmk.utils.debug.enabled():
990 raise
991 return []
994 def rbn_emails_contacts(emails):
995 return ["mailto:" + e for e in emails]
999 # .--Flexible-Notifications----------------------------------------------.
1000 # | _____ _ _ _ _ |
1001 # | | ___| | _____ _(_) |__ | | ___ |
1002 # | | |_ | |/ _ \ \/ / | '_ \| |/ _ \ |
1003 # | | _| | | __/> <| | |_) | | __/ |
1004 # | |_| |_|\___/_/\_\_|_.__/|_|\___| |
1005 # | |
1006 # +----------------------------------------------------------------------+
1007 # | Implementation of the pre 1.2.5, hopelessly outdated flexible |
1008 # | notifications. |
1009 # '----------------------------------------------------------------------'
1012 def notify_flexible(raw_context, notification_table):
1014 for entry in notification_table:
1015 plugin = entry["plugin"]
1016 notify_log(" Notification channel with plugin %s" % (plugin or "plain email"))
1018 if not should_notify(raw_context, entry):
1019 continue
1021 plugin_context = create_plugin_context(raw_context, entry.get("parameters", []))
1023 if config.notification_spooling in ("local", "both"):
1024 create_spoolfile({"context": plugin_context, "plugin": plugin})
1025 else:
1026 call_notification_script(plugin, plugin_context)
1029 # may return
1030 # 0 : everything fine -> proceed
1031 # 1 : currently not OK -> try to process later on
1032 # >=2: invalid -> discard
1033 def should_notify(context, entry):
1034 # Check disabling
1035 if entry.get("disabled"):
1036 notify_log(" - Skipping: it is disabled for this user")
1037 return False
1039 # Check host, if configured
1040 if entry.get("only_hosts"):
1041 hostname = context.get("HOSTNAME")
1043 skip = True
1044 regex_match = False
1045 negate = False
1046 for h in entry["only_hosts"]:
1047 if h.startswith("!"): # negate
1048 negate = True
1049 h = h[1:]
1050 elif h.startswith('~'):
1051 regex_match = True
1052 h = h[1:]
1054 if not regex_match and hostname == h:
1055 skip = negate
1056 break
1058 elif regex_match and re.match(h, hostname):
1059 skip = negate
1060 break
1061 if skip:
1062 notify_log(" - Skipping: host '%s' matches none of %s" % (hostname, ", ".join(
1063 entry["only_hosts"])))
1064 return False
1066 # Check if the host has to be in a special service_level
1067 if "match_sl" in entry:
1068 from_sl, to_sl = entry['match_sl']
1069 if context['WHAT'] == "SERVICE" and context.get('SVC_SL', '').isdigit():
1070 sl = events.saveint(context.get('SVC_SL'))
1071 else:
1072 sl = events.saveint(context.get('HOST_SL'))
1074 if sl < from_sl or sl > to_sl:
1075 notify_log(" - Skipping: service level %d not between %d and %d" % (sl, from_sl, to_sl))
1076 return False
1078 # Skip blacklistet serivces
1079 if entry.get("service_blacklist"):
1080 servicedesc = context.get("SERVICEDESC")
1081 if not servicedesc:
1082 notify_log(" - Proceed: blacklist certain services, but this is a host notification")
1083 else:
1084 for s in entry["service_blacklist"]:
1085 if re.match(s, servicedesc):
1086 notify_log(" - Skipping: service '%s' matches blacklist (%s)" %
1087 (servicedesc, ", ".join(entry["service_blacklist"])))
1088 return False
1090 # Check service, if configured
1091 if entry.get("only_services"):
1092 servicedesc = context.get("SERVICEDESC")
1093 if not servicedesc:
1094 notify_log(" - Proceed: limited to certain services, but this is a host notification")
1095 else:
1096 # Example
1097 # only_services = [ "!LOG foo", "LOG", BAR" ]
1098 # -> notify all services beginning with LOG or BAR, but not "LOG foo..."
1099 skip = True
1100 for s in entry["only_services"]:
1101 if s.startswith("!"): # negate
1102 negate = True
1103 s = s[1:]
1104 else:
1105 negate = False
1106 if re.match(s, servicedesc):
1107 skip = negate
1108 break
1109 if skip:
1110 notify_log(" - Skipping: service '%s' matches none of %s" % (servicedesc, ", ".join(
1111 entry["only_services"])))
1112 return False
1114 # Check notification type
1115 event, allowed_events = check_notification_type(context, entry["host_events"],
1116 entry["service_events"])
1117 if event not in allowed_events:
1118 notify_log(" - Skipping: wrong notification type %s (%s), only %s are allowed" %
1119 (event, context["NOTIFICATIONTYPE"], ",".join(allowed_events)))
1120 return False
1122 # Check notification number (in case of repeated notifications/escalations)
1123 if "escalation" in entry:
1124 from_number, to_number = entry["escalation"]
1125 if context["WHAT"] == "HOST":
1126 notification_number = int(context.get("HOSTNOTIFICATIONNUMBER", 1))
1127 else:
1128 notification_number = int(context.get("SERVICENOTIFICATIONNUMBER", 1))
1129 if notification_number < from_number or notification_number > to_number:
1130 notify_log(" - Skipping: notification number %d does not lie in range %d ... %d" %
1131 (notification_number, from_number, to_number))
1132 return False
1134 if "timeperiod" in entry:
1135 timeperiod = entry["timeperiod"]
1136 if timeperiod and timeperiod != "24X7":
1137 if not cmk_base.core.check_timeperiod(timeperiod):
1138 notify_log(" - Skipping: time period %s is currently not active" % timeperiod)
1139 return False
1140 return True
1143 def check_notification_type(context, host_events, service_events):
1144 notification_type = context["NOTIFICATIONTYPE"]
1145 if context["WHAT"] == "HOST":
1146 allowed_events = host_events
1147 state = context["HOSTSTATE"]
1148 event_map = {"UP": 'r', "DOWN": 'd', "UNREACHABLE": 'u'}
1149 else:
1150 allowed_events = service_events
1151 state = context["SERVICESTATE"]
1152 event_map = {"OK": 'r', "WARNING": 'w', "CRITICAL": 'c', "UNKNOWN": 'u'}
1154 if notification_type == "RECOVERY":
1155 event = 'r'
1156 elif notification_type in ["FLAPPINGSTART", "FLAPPINGSTOP", "FLAPPINGDISABLED"]:
1157 event = 'f'
1158 elif notification_type in ["DOWNTIMESTART", "DOWNTIMEEND", "DOWNTIMECANCELLED"]:
1159 event = 's'
1160 elif notification_type == "ACKNOWLEDGEMENT":
1161 event = 'x'
1162 else:
1163 event = event_map.get(state, '?')
1165 return event, allowed_events
1169 # .--Plain Email---------------------------------------------------------.
1170 # | ____ _ _ _____ _ _ |
1171 # | | _ \| | __ _(_)_ __ | ____|_ __ ___ __ _(_) | |
1172 # | | |_) | |/ _` | | '_ \ | _| | '_ ` _ \ / _` | | | |
1173 # | | __/| | (_| | | | | | | |___| | | | | | (_| | | | |
1174 # | |_| |_|\__,_|_|_| |_| |_____|_| |_| |_|\__,_|_|_| |
1175 # | |
1176 # +----------------------------------------------------------------------+
1177 # | Plain Email notification, inline implemented. This is also being |
1178 # | used as a pseudo-plugin by Flexible Notification and RBN. |
1179 # '----------------------------------------------------------------------'
1182 def notify_plain_email(raw_context):
1183 plugin_context = create_plugin_context(raw_context, [])
1185 if config.notification_spooling in ("local", "both"):
1186 create_spoolfile({"context": plugin_context, "plugin": None})
1187 else:
1188 notify_log("Sending plain email to %s" % plugin_context["CONTACTNAME"])
1189 notify_via_email(plugin_context)
1192 def notify_via_email(plugin_context):
1193 notify_log(substitute_context(notification_log_template, plugin_context))
1195 if plugin_context["WHAT"] == "SERVICE":
1196 subject_t = notification_service_subject
1197 body_t = notification_service_body
1198 else:
1199 subject_t = notification_host_subject
1200 body_t = notification_host_body
1202 subject = substitute_context(subject_t, plugin_context)
1203 plugin_context["SUBJECT"] = subject
1204 body = substitute_context(notification_common_body + body_t, plugin_context)
1205 command = substitute_context(notification_mail_command, plugin_context)
1206 command_utf8 = command.encode("utf-8")
1208 # Make sure that mail(x) is using UTF-8. Otherwise we cannot send notifications
1209 # with non-ASCII characters. Unfortunately we do not know whether C.UTF-8 is
1210 # available. If e.g. mail detects a non-Ascii character in the mail body and
1211 # the specified encoding is not available, it will silently not send the mail!
1212 # Our resultion in future: use /usr/sbin/sendmail directly.
1213 # Our resultion in the present: look with locale -a for an existing UTF encoding
1214 # and use that.
1215 old_lang = os.getenv("LANG", "")
1216 for encoding in os.popen("locale -a 2>/dev/null"):
1217 l = encoding.lower()
1218 if "utf8" in l or "utf-8" in l or "utf.8" in l:
1219 encoding = encoding.strip()
1220 os.putenv("LANG", encoding)
1221 notify_log_debug("Setting locale for mail to %s." % encoding)
1222 break
1223 else:
1224 notify_log("No UTF-8 encoding found in your locale -a! Please provide C.UTF-8 encoding.")
1226 # Important: we must not output anything on stdout or stderr. Data of stdout
1227 # goes back into the socket to the CMC in keepalive mode and garbles the
1228 # handshake signal.
1229 notify_log_debug("Executing command: %s" % command)
1231 # TODO: Cleanup this shell=True call!
1232 p = subprocess.Popen( # nosec
1233 command_utf8,
1234 shell=True,
1235 stdout=subprocess.PIPE,
1236 stderr=subprocess.PIPE,
1237 stdin=subprocess.PIPE,
1238 close_fds=True)
1239 stdout_txt, stderr_txt = p.communicate(body.encode("utf-8"))
1240 exitcode = p.returncode
1241 os.putenv("LANG", old_lang) # Important: do not destroy our environment
1242 if exitcode != 0:
1243 notify_log("ERROR: could not deliver mail. Exit code of command is %r" % exitcode)
1244 for line in (stdout_txt + stderr_txt).splitlines():
1245 notify_log("mail: %s" % line.rstrip())
1246 return 2
1248 return 0
1252 # .--Plugins-------------------------------------------------------------.
1253 # | ____ _ _ |
1254 # | | _ \| |_ _ __ _(_)_ __ ___ |
1255 # | | |_) | | | | |/ _` | | '_ \/ __| |
1256 # | | __/| | |_| | (_| | | | | \__ \ |
1257 # | |_| |_|\__,_|\__, |_|_| |_|___/ |
1258 # | |___/ |
1259 # +----------------------------------------------------------------------+
1260 # | Code for the actuall calling of notification plugins (scripts). |
1261 # '----------------------------------------------------------------------'
1263 # Exit codes for plugins and also for our functions that call the plugins:
1264 # 0: Notification successfully sent
1265 # 1: Could not send now, please retry later
1266 # 2: Cannot send, retry does not make sense
1269 # Add the plugin parameters to the envinroment. We have two types of parameters:
1270 # - list, the legacy style. This will lead to PARAMETERS_1, ...
1271 # - dict, the new style for scripts with WATO rule. This will lead to
1272 # PARAMETER_FOO_BAR for a dict key named "foo_bar".
1273 def create_plugin_context(raw_context, params):
1274 plugin_context = {}
1275 plugin_context.update(raw_context) # Make a real copy
1276 events.add_to_event_context(plugin_context, "PARAMETER", params, log_function=notify_log)
1277 return plugin_context
1280 def create_bulk_parameter_context(params):
1281 dict_context = create_plugin_context({}, params)
1282 return [
1283 "%s=%s\n" % (varname, value.replace("\r", "").replace("\n", "\1"))
1284 for (varname, value) in dict_context.items()
1288 def path_to_notification_script(plugin):
1289 # Call actual script without any arguments
1290 local_path = cmk.utils.paths.local_notifications_dir + "/" + plugin
1291 if os.path.exists(local_path):
1292 path = local_path
1293 else:
1294 path = cmk.utils.paths.notifications_dir + "/" + plugin
1296 if not os.path.exists(path):
1297 notify_log("Notification plugin '%s' not found" % plugin)
1298 notify_log(" not in %s" % cmk.utils.paths.notifications_dir)
1299 notify_log(" and not in %s" % cmk.utils.paths.local_notifications_dir)
1300 return None
1302 return path
1305 # This is the function that finally sends the actual notification.
1306 # It does this by calling an external script are creating a
1307 # plain email and calling bin/mail.
1309 # It also does the central logging of the notifications
1310 # that are actually sent out.
1312 # Note: this function is *not* being called for bulk notification.
1313 def call_notification_script(plugin, plugin_context):
1314 _log_to_history(notification_message(plugin or "plain email", plugin_context))
1316 def plugin_log(s):
1317 notify_log(" %s" % s)
1319 # The "Pseudo"-Plugin None means builtin plain email
1320 if not plugin:
1321 return notify_via_email(plugin_context)
1323 # Call actual script without any arguments
1324 path = path_to_notification_script(plugin)
1325 if not path:
1326 return 2
1328 plugin_log("executing %s" % path)
1329 try:
1330 set_notification_timeout()
1331 p = subprocess.Popen([path],
1332 stdout=subprocess.PIPE,
1333 stderr=subprocess.STDOUT,
1334 env=notification_script_env(plugin_context),
1335 close_fds=True)
1337 while True:
1338 # read and output stdout linewise to ensure we don't force python to produce
1339 # one - potentially huge - memory buffer
1340 line = p.stdout.readline()
1341 if line != '':
1342 plugin_log("Output: %s" % line.decode('utf-8').rstrip())
1343 if _log_to_stdout:
1344 console.output(line)
1345 else:
1346 break
1347 # the stdout is closed but the return code may not be available just yet - wait for the
1348 # process to actually finish
1349 exitcode = p.wait()
1350 clear_notification_timeout()
1351 except NotificationTimeout:
1352 plugin_log("Notification plugin did not finish within %d seconds. Terminating." %
1353 config.notification_plugin_timeout)
1354 # p.kill() requires python 2.6!
1355 os.kill(p.pid, signal.SIGTERM)
1356 exitcode = 1
1358 if exitcode != 0:
1359 plugin_log("Plugin exited with code %d" % exitcode)
1361 return exitcode
1364 # Construct the environment for the notification script
1365 def notification_script_env(plugin_context):
1366 # Use half of the maximum allowed string length MAX_ARG_STRLEN
1367 # which is usually 32 pages on Linux (see "man execve").
1369 # Assumption: We don't have to consider ARG_MAX, i.e. the maximum
1370 # size of argv + envp, because it is derived from RLIMIT_STACK
1371 # and large enough.
1372 try:
1373 max_length = 32 * os.sysconf("SC_PAGESIZE") // 2
1374 except ValueError:
1375 max_length = 32 * 4046 // 2
1377 def format_(value):
1378 if len(value) > max_length:
1379 value = value[:max_length] + "...\nAttention: Removed remaining content because it was too long."
1380 return value.encode("utf-8")
1382 notify_env = {
1383 "NOTIFY_" + variable: format_(value) for variable, value in plugin_context.iteritems()
1385 notify_env.update(os.environ)
1386 return notify_env
1389 class NotificationTimeout(Exception):
1390 pass
1393 def handle_notification_timeout(signum, frame):
1394 raise NotificationTimeout()
1397 def set_notification_timeout():
1398 signal.signal(signal.SIGALRM, handle_notification_timeout)
1399 signal.alarm(config.notification_plugin_timeout)
1402 def clear_notification_timeout():
1403 signal.alarm(0)
1407 # .--Spooling------------------------------------------------------------.
1408 # | ____ _ _ |
1409 # | / ___| _ __ ___ ___ | (_)_ __ __ _ |
1410 # | \___ \| '_ \ / _ \ / _ \| | | '_ \ / _` | |
1411 # | ___) | |_) | (_) | (_) | | | | | | (_| | |
1412 # | |____/| .__/ \___/ \___/|_|_|_| |_|\__, | |
1413 # | |_| |___/ |
1414 # +----------------------------------------------------------------------+
1415 # | Some functions dealing with the spooling of notifications. |
1416 # '----------------------------------------------------------------------'
1419 def create_spoolfile(data):
1420 if not os.path.exists(notification_spooldir):
1421 os.makedirs(notification_spooldir)
1422 file_path = "%s/%s" % (notification_spooldir, fresh_uuid())
1423 notify_log("Creating spoolfile: %s" % file_path)
1425 # First write into tempfile that is not handled by mknotifyd
1426 file(file_path + ".new", "w").write(pprint.pformat(data))
1427 os.rename(file_path + ".new", file_path)
1430 # There are three types of spool files:
1431 # 1. Notifications to be forwarded. Contain key "forward"
1432 # 2. Notifications for async local delivery. Contain key "plugin"
1433 # 3. Notifications that *were* forwarded (e.g. received from a slave). Contain neither of both.
1434 # Spool files of type 1 are not handled here!
1435 def handle_spoolfile(spoolfile):
1436 notif_uuid = spoolfile.rsplit("/", 1)[-1]
1437 notify_log("----------------------------------------------------------------------")
1438 try:
1439 data = eval(file(spoolfile).read())
1440 if "plugin" in data:
1441 plugin_context = data["context"]
1442 plugin = data["plugin"]
1443 notify_log("Got spool file %s (%s) for local delivery via %s" %
1444 (notif_uuid[:8], events.find_host_service_in_context(plugin_context),
1445 (plugin or "plain mail")))
1446 return call_notification_script(plugin, plugin_context)
1448 # We received a forwarded raw notification. We need to process
1449 # this with our local notification rules in order to call one,
1450 # several or no actual plugins.
1451 raw_context = data["context"]
1452 notify_log("Got spool file %s (%s) from remote host for local delivery." %
1453 (notif_uuid[:8], events.find_host_service_in_context(raw_context)))
1455 store_notification_backlog(data["context"])
1456 locally_deliver_raw_context(data["context"])
1457 return 0 # No error handling for async delivery
1459 except Exception as e:
1460 notify_log("ERROR %s\n%s" % (e, format_exception()))
1461 return 2
1465 # .--Bulk-Notifications--------------------------------------------------.
1466 # | ____ _ _ |
1467 # | | __ ) _ _| | | __ |
1468 # | | _ \| | | | | |/ / |
1469 # | | |_) | |_| | | < |
1470 # | |____/ \__,_|_|_|\_\ |
1471 # | |
1472 # +----------------------------------------------------------------------+
1473 # | Store postponed bulk notifications for later delivery. Deliver such |
1474 # | notifications on cmk --notify bulk. |
1475 # '----------------------------------------------------------------------'
1478 def do_bulk_notify(plugin, params, plugin_context, bulk):
1479 # First identify the bulk. The following elements identify it:
1480 # 1. contact
1481 # 2. plugin
1482 # 3. time horizon (interval) in seconds
1483 # 4. max bulked notifications
1484 # 5. elements specified in bulk["groupby"] and bulk["groupby_custom"]
1485 # We first create a bulk path constructed as a tuple of strings.
1486 # Later we convert that to a unique directory name.
1487 # Note: if you have separate bulk rules with exactly the same
1488 # bulking options, then they will use the same bulk.
1490 what = plugin_context["WHAT"]
1491 contact = plugin_context["CONTACTNAME"]
1492 if bulk.get("timeperiod"):
1493 bulk_path = (contact, plugin, 'timeperiod:' + bulk["timeperiod"], str(bulk["count"]))
1494 else:
1495 bulk_path = (contact, plugin, str(bulk["interval"]), str(bulk["count"]))
1496 bulkby = bulk["groupby"]
1498 if "bulk_subject" in bulk:
1499 plugin_context["PARAMETER_BULK_SUBJECT"] = bulk["bulk_subject"]
1501 if "host" in bulkby:
1502 bulk_path += ("host", plugin_context["HOSTNAME"])
1504 elif "folder" in bulkby:
1505 bulk_path += ("folder", find_wato_folder(plugin_context))
1507 if "service" in bulkby:
1508 bulk_path += ("service", plugin_context.get("SERVICEDESC", ""))
1510 if "sl" in bulkby:
1511 bulk_path += ("sl", plugin_context.get(what + "_SL", ""))
1513 if "check_type" in bulkby:
1514 bulk_path += ("check_type", plugin_context.get(what + "CHECKCOMMAND", "").split("!")[0])
1516 if "state" in bulkby:
1517 bulk_path += ("state", plugin_context.get(what + "STATE", ""))
1519 if "ec_contact" in bulkby:
1520 bulk_path += ("ec_contact", plugin_context.get("EC_CONTACT", ""))
1522 if "ec_comment" in bulkby:
1523 bulk_path += ("ec_comment", plugin_context.get("EC_COMMENT", ""))
1525 # User might have specified _FOO instead of FOO
1526 bulkby_custom = bulk.get("groupby_custom", [])
1527 for macroname in bulkby_custom:
1528 macroname = macroname.lstrip("_").upper()
1529 value = plugin_context.get(what + "_" + macroname, "")
1530 bulk_path += (macroname.lower(), value)
1532 notify_log(" --> storing for bulk notification %s" % "|".join(bulk_path))
1533 bulk_dirname = create_bulk_dirname(bulk_path)
1534 uuid = fresh_uuid()
1535 filename = bulk_dirname + "/" + uuid
1536 file(filename + ".new", "w").write("%r\n" % ((params, plugin_context),))
1537 os.rename(filename + ".new", filename) # We need an atomic creation!
1538 notify_log(" - stored in %s" % filename)
1541 def find_wato_folder(context):
1542 for tag in context.get("HOSTTAGS", "").split():
1543 if tag.startswith("/wato/"):
1544 return tag[6:].rstrip("/")
1545 return ""
1548 def create_bulk_dirname(bulk_path):
1549 dirname = os.path.join(notification_bulkdir, bulk_path[0], bulk_path[1],
1550 ",".join([b.replace("/", "\\") for b in bulk_path[2:]]))
1552 # Remove non-Ascii-characters by special %02x-syntax
1553 try:
1554 str(dirname)
1555 except:
1556 new_dirname = ""
1557 for char in dirname:
1558 if ord(char) <= 0 or ord(char) > 127:
1559 new_dirname += "%%%04x" % ord(char)
1560 else:
1561 new_dirname += char
1562 dirname = new_dirname
1564 if not os.path.exists(dirname):
1565 os.makedirs(dirname)
1566 notify_log(" - created bulk directory %s" % dirname)
1567 return dirname
1570 def bulk_parts(method_dir, bulk):
1571 parts = bulk.split(',')
1573 try:
1574 interval, timeperiod = int(parts[0]), None
1575 except ValueError:
1576 entry = parts[0].split(':')
1577 if entry[0] == 'timeperiod' and len(entry) == 2:
1578 interval, timeperiod = None, entry[1]
1579 else:
1580 notify_log("Skipping invalid bulk directory %s" % method_dir)
1581 return None
1583 try:
1584 count = int(parts[1])
1585 except ValueError:
1586 notify_log("Skipping invalid bulk directory %s" % method_dir)
1587 return None
1589 return interval, timeperiod, count
1592 def bulk_uuids(bulk_dir):
1593 uuids, oldest = [], time.time()
1594 for uuid in os.listdir(bulk_dir): # 4ded0fa2-f0cd-4b6a-9812-54374a04069f
1595 if uuid.endswith(".new"):
1596 continue
1597 if len(uuid) != 36:
1598 notify_log("Skipping invalid notification file %s" % os.path.join(bulk_dir, uuid))
1599 continue
1601 mtime = os.stat(os.path.join(bulk_dir, uuid)).st_mtime
1602 uuids.append((mtime, uuid))
1603 oldest = min(oldest, mtime)
1604 uuids.sort()
1605 return uuids, oldest
1608 def remove_if_orphaned(bulk_dir, max_age, ref_time=None):
1609 if not ref_time:
1610 ref_time = time.time()
1612 dirage = ref_time - os.stat(bulk_dir).st_mtime
1613 if dirage > max_age:
1614 notify_log("Warning: removing orphaned empty bulk directory %s" % bulk_dir)
1615 try:
1616 os.rmdir(bulk_dir)
1617 except Exception as e:
1618 notify_log(" -> Error removing it: %s" % e)
1621 def find_bulks(only_ripe):
1622 if not os.path.exists(notification_bulkdir):
1623 return []
1625 def listdir_visible(path):
1626 return [x for x in os.listdir(path) if not x.startswith(".")]
1628 bulks, now = [], time.time()
1629 for contact in listdir_visible(notification_bulkdir):
1630 contact_dir = os.path.join(notification_bulkdir, contact)
1631 for method in listdir_visible(contact_dir):
1632 method_dir = os.path.join(contact_dir, method)
1633 for bulk in listdir_visible(method_dir):
1634 bulk_dir = os.path.join(method_dir, bulk)
1636 uuids, oldest = bulk_uuids(bulk_dir)
1637 if not uuids:
1638 remove_if_orphaned(bulk_dir, max_age=60, ref_time=now)
1639 continue
1640 age = now - oldest
1642 # e.g. 60,10,host,localhost OR timeperiod:late_night,1000,host,localhost
1643 parts = bulk_parts(method_dir, bulk)
1644 if not parts:
1645 continue
1646 interval, timeperiod, count = parts
1648 if interval is not None:
1649 if age >= interval:
1650 notify_log("Bulk %s is ripe: age %d >= %d" % (bulk_dir, age, interval))
1651 elif len(uuids) >= count:
1652 notify_log(
1653 "Bulk %s is ripe: count %d >= %d" % (bulk_dir, len(uuids), count))
1654 else:
1655 notify_log("Bulk %s is not ripe yet (age: %d, count: %d)!" % (bulk_dir, age,
1656 len(uuids)))
1657 if only_ripe:
1658 continue
1660 bulks.append((bulk_dir, age, interval, 'n.a.', count, uuids))
1661 else:
1662 try:
1663 active = cmk_base.core.timeperiod_active(timeperiod)
1664 except:
1665 # This prevents sending bulk notifications if a
1666 # livestatus connection error appears. It also implies
1667 # that an ongoing connection error will hold back bulk
1668 # notifications.
1669 notify_log("Error while checking activity of timeperiod %s: assuming active"
1670 % timeperiod)
1671 active = True
1673 if active is True and len(uuids) < count:
1674 # Only add a log entry every 10 minutes since timeperiods
1675 # can be very long (The default would be 10s).
1676 if now % 600 <= config.notification_bulk_interval:
1677 notify_log("Bulk %s is not ripe yet (timeperiod %s: active, count: %d)"
1678 % (bulk_dir, timeperiod, len(uuids)))
1680 if only_ripe:
1681 continue
1682 elif active is False:
1683 notify_log(
1684 "Bulk %s is ripe: timeperiod %s has ended" % (bulk_dir, timeperiod))
1685 elif len(uuids) >= count:
1686 notify_log(
1687 "Bulk %s is ripe: count %d >= %d" % (bulk_dir, len(uuids), count))
1688 else:
1689 notify_log("Bulk %s is ripe: timeperiod %s is not known anymore" %
1690 (bulk_dir, timeperiod))
1692 bulks.append((bulk_dir, age, 'n.a.', timeperiod, count, uuids))
1694 return bulks
1697 def send_ripe_bulks():
1698 ripe = find_bulks(True)
1699 if ripe:
1700 notify_log("Sending out %d ripe bulk notifications" % len(ripe))
1701 for bulk in ripe:
1702 try:
1703 notify_bulk(bulk[0], bulk[-1])
1704 except Exception:
1705 if cmk.utils.debug.enabled():
1706 raise
1707 notify_log("Error sending bulk %s: %s" % (bulk[0], format_exception()))
1710 def notify_bulk(dirname, uuids):
1711 parts = dirname.split("/")
1712 contact = parts[-3]
1713 plugin = parts[-2]
1714 notify_log(" -> %s/%s %s" % (contact, plugin, dirname))
1715 # If new entries are created in this directory while we are working
1716 # on it, nothing bad happens. It's just that we cannot remove
1717 # the directory after our work. It will be the starting point for
1718 # the next bulk with the same ID, which is completely OK.
1719 bulk_context = []
1720 old_params = None
1721 unhandled_uuids = []
1722 for mtime, uuid in uuids:
1723 try:
1724 params, context = eval(file(dirname + "/" + uuid).read())
1725 except Exception as e:
1726 if cmk.utils.debug.enabled():
1727 raise
1728 notify_log(" Deleting corrupted or empty bulk file %s/%s: %s" % (dirname, uuid, e))
1729 continue
1731 if old_params is None:
1732 old_params = params
1733 elif params != old_params:
1734 notify_log(" Parameters are different from previous, postponing into separate bulk")
1735 unhandled_uuids.append((mtime, uuid))
1736 continue
1738 part_block = []
1739 part_block.append("\n")
1740 for varname, value in context.items():
1741 part_block.append("%s=%s\n" % (varname, value.replace("\r", "").replace("\n", "\1")))
1742 bulk_context.append(part_block)
1744 # Do not forget to add this to the monitoring log. We create
1745 # a single entry for each notification contained in the bulk.
1746 # It is important later to have this precise information.
1747 plugin_name = "bulk " + (plugin or "plain email")
1748 _log_to_history(notification_message(plugin_name, context))
1750 if bulk_context: # otherwise: only corrupted files
1751 # Per default the uuids are sorted chronologically from oldest to newest
1752 # Therefore the notification plugin also shows the oldest entry first
1753 # The following configuration option allows to reverse the sorting
1754 if isinstance(old_params, dict) and old_params.get("bulk_sort_order") == "newest_first":
1755 bulk_context.reverse()
1757 # Converts bulk context from [[1,2],[3,4]] to [1,2,3,4]
1758 bulk_context = [x for y in bulk_context for x in y]
1760 parameter_context = create_bulk_parameter_context(old_params)
1761 context_text = "".join(parameter_context + bulk_context)
1762 call_bulk_notification_script(plugin, context_text)
1763 else:
1764 notify_log("No valid notification file left. Skipping this bulk.")
1766 # Remove sent notifications
1767 for mtime, uuid in uuids:
1768 if (mtime, uuid) not in unhandled_uuids:
1769 path = os.path.join(dirname, uuid)
1770 try:
1771 os.remove(path)
1772 except Exception as e:
1773 notify_log("Cannot remove %s: %s" % (path, e))
1775 # Repeat with unhandled uuids (due to different parameters)
1776 if unhandled_uuids:
1777 notify_bulk(dirname, unhandled_uuids)
1779 # Remove directory. Not neccessary if emtpy
1780 try:
1781 os.rmdir(dirname)
1782 except Exception as e:
1783 if not unhandled_uuids:
1784 notify_log("Warning: cannot remove directory %s: %s" % (dirname, e))
1787 def call_bulk_notification_script(plugin, context_text):
1788 path = path_to_notification_script(plugin)
1789 if not path:
1790 raise MKGeneralException("Notification plugin %s not found" % plugin)
1792 stdout_txt = stderr_txt = ""
1793 try:
1794 set_notification_timeout()
1796 # Protocol: The script gets the context on standard input and
1797 # read until that is closed. It is being called with the parameter
1798 # --bulk.
1799 p = subprocess.Popen([path, "--bulk"],
1800 stdout=subprocess.PIPE,
1801 stderr=subprocess.PIPE,
1802 stdin=subprocess.PIPE,
1803 close_fds=True)
1805 stdout_txt, stderr_txt = p.communicate(context_text.encode("utf-8"))
1806 exitcode = p.returncode
1808 clear_notification_timeout()
1809 except NotificationTimeout:
1810 notify_log("Notification plugin did not finish within %d seconds. Terminating." %
1811 config.notification_plugin_timeout)
1812 # p.kill() requires python 2.6!
1813 os.kill(p.pid, signal.SIGTERM)
1814 exitcode = 1
1816 if exitcode:
1817 notify_log("ERROR: script %s --bulk returned with exit code %s" % (path, exitcode))
1818 for line in (stdout_txt + stderr_txt).splitlines():
1819 notify_log("%s: %s" % (plugin, line.rstrip()))
1823 # .--Contexts------------------------------------------------------------.
1824 # | ____ _ _ |
1825 # | / ___|___ _ __ | |_ _____ _| |_ ___ |
1826 # | | | / _ \| '_ \| __/ _ \ \/ / __/ __| |
1827 # | | |__| (_) | | | | || __/> <| |_\__ \ |
1828 # | \____\___/|_| |_|\__\___/_/\_\\__|___/ |
1829 # | |
1830 # +----------------------------------------------------------------------+
1831 # | Functions dealing with loading, storing and converting contexts. |
1832 # '----------------------------------------------------------------------'
1835 # Be aware: The backlog.mk contains the raw context which has not been decoded
1836 # to unicode yet. It contains raw encoded strings e.g. the plugin output provided
1837 # by third party plugins which might be UTF-8 encoded but can also be encoded in
1838 # other ways. Currently the context is converted later by bot, this module
1839 # and the GUI. TODO Maybe we should centralize the encoding here and save the
1840 # backlock already encoded.
1841 def store_notification_backlog(raw_context):
1842 path = notification_logdir + "/backlog.mk"
1843 if not config.notification_backlog:
1844 if os.path.exists(path):
1845 os.remove(path)
1846 return
1848 try:
1849 backlog = eval(file(path).read())[:config.notification_backlog - 1]
1850 except:
1851 backlog = []
1853 backlog = [raw_context] + backlog
1854 file(path, "w").write("%r\n" % backlog)
1857 def raw_context_from_backlog(nr):
1858 try:
1859 backlog = eval(file(notification_logdir + "/backlog.mk").read())
1860 except:
1861 backlog = []
1863 if nr < 0 or nr >= len(backlog):
1864 console.error("No notification number %d in backlog.\n" % nr)
1865 sys.exit(2)
1867 notify_log("Replaying notification %d from backlog...\n" % nr)
1868 return backlog[nr]
1871 def raw_context_from_env():
1872 # Information about notification is excpected in the
1873 # environment in variables with the prefix NOTIFY_
1874 return dict([(var[7:], value)
1875 for (var, value) in os.environ.items()
1876 if var.startswith("NOTIFY_") and not dead_nagios_variable(value)])
1879 def substitute_context(template, context):
1880 # First replace all known variables
1881 for varname, value in context.items():
1882 template = template.replace('$' + varname + '$', value)
1884 # Remove the rest of the variables and make them empty
1885 template = re.sub(r"\$[A-Z]+\$", "", template)
1886 return template
1890 # .--Helpers-------------------------------------------------------------.
1891 # | _ _ _ |
1892 # | | | | | ___| |_ __ ___ _ __ ___ |
1893 # | | |_| |/ _ \ | '_ \ / _ \ '__/ __| |
1894 # | | _ | __/ | |_) | __/ | \__ \ |
1895 # | |_| |_|\___|_| .__/ \___|_| |___/ |
1896 # | |_| |
1897 # +----------------------------------------------------------------------+
1898 # | Some generic helper functions |
1899 # '----------------------------------------------------------------------'
1902 def format_exception():
1903 import traceback
1904 import StringIO
1906 txt = StringIO.StringIO()
1907 t, v, tb = sys.exc_info()
1908 traceback.print_exception(t, v, tb, None, txt)
1909 return txt.getvalue()
1912 def dead_nagios_variable(value):
1913 if len(value) < 3:
1914 return False
1915 if value[0] != '$' or value[-1] != '$':
1916 return False
1917 for c in value[1:-1]:
1918 if not c.isupper() and c != '_':
1919 return False
1920 return True
1923 def notify_log(message):
1924 if config.notification_logging <= 20:
1925 events.event_log(notification_log, message)
1928 def notify_log_verbose(message):
1929 if config.notification_logging <= 15:
1930 events.event_log(notification_log, message)
1933 def notify_log_debug(message):
1934 if config.notification_logging <= 10:
1935 events.event_log(notification_log, message)
1938 def fresh_uuid():
1939 try:
1940 return file('/proc/sys/kernel/random/uuid').read().strip()
1941 except IOError:
1942 # On platforms where the above file does not exist we try to
1943 # use the python uuid module which seems to be a good fallback
1944 # for those systems. Well, if got python < 2.5 you are lost for now.
1945 import uuid
1946 return str(uuid.uuid4())
1949 def _log_to_history(message):
1950 _livestatus_cmd("LOG;%s" % message)
1953 def _livestatus_cmd(command):
1954 try:
1955 livestatus.LocalConnection().command("[%d] %s" % (time.time(), command.encode("utf-8")))
1956 except Exception as e:
1957 if cmk.utils.debug.enabled():
1958 raise
1959 notify_log("WARNING: cannot send livestatus command: %s" % e)
1960 notify_log("Command was: %s" % command)