Refactoring: Moved check parameters from unsorted.py to dedicated modules (CMK-1393)
[check_mk.git] / cmk_base / notify.py
blob46396cd5c149de38d68a601eb593e063e1d997f4
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.regex import regex
51 import cmk.utils.paths
52 from cmk.utils.exceptions import MKGeneralException
54 import cmk_base.utils
55 import cmk_base.config as config
56 import cmk_base.console as console
57 import cmk_base.core
58 import cmk_base.events as events
60 try:
61 import cmk_base.cee.keepalive as keepalive
62 except ImportError:
63 keepalive = None # type: ignore
65 _log_to_stdout = False
66 notify_mode = "notify"
68 # .--Configuration-------------------------------------------------------.
69 # | ____ __ _ _ _ |
70 # | / ___|___ _ __ / _(_) __ _ _ _ _ __ __ _| |_(_) ___ _ __ |
71 # | | | / _ \| '_ \| |_| |/ _` | | | | '__/ _` | __| |/ _ \| '_ \ |
72 # | | |__| (_) | | | | _| | (_| | |_| | | | (_| | |_| | (_) | | | | |
73 # | \____\___/|_| |_|_| |_|\__, |\__,_|_| \__,_|\__|_|\___/|_| |_| |
74 # | |___/ |
75 # +----------------------------------------------------------------------+
76 # | Default values of global configuration variables. |
77 # '----------------------------------------------------------------------'
79 # Default settings
80 notification_logdir = cmk.utils.paths.var_dir + "/notify"
81 notification_spooldir = cmk.utils.paths.var_dir + "/notify/spool"
82 notification_bulkdir = cmk.utils.paths.var_dir + "/notify/bulk"
83 notification_core_log = cmk.utils.paths.var_dir + "/notify/nagios.log" # Fallback for history if no CMC running
84 notification_log = cmk.utils.paths.log_dir + "/notify.log"
86 notification_log_template = \
87 u"$CONTACTNAME$ - $NOTIFICATIONTYPE$ - " \
88 u"$HOSTNAME$ $HOSTSTATE$ - " \
89 u"$SERVICEDESC$ $SERVICESTATE$ "
91 notification_mail_command = u"mail -s '$SUBJECT$' '$CONTACTEMAIL$'"
92 notification_host_subject = u"Check_MK: $HOSTNAME$ - $NOTIFICATIONTYPE$"
93 notification_service_subject = u"Check_MK: $HOSTNAME$/$SERVICEDESC$ $NOTIFICATIONTYPE$"
95 notification_common_body = u"""Host: $HOSTNAME$
96 Alias: $HOSTALIAS$
97 Address: $HOSTADDRESS$
98 """
100 notification_host_body = u"""State: $LASTHOSTSTATE$ -> $HOSTSTATE$ ($NOTIFICATIONTYPE$)
101 Command: $HOSTCHECKCOMMAND$
102 Output: $HOSTOUTPUT$
103 Perfdata: $HOSTPERFDATA$
104 $LONGHOSTOUTPUT$
107 notification_service_body = u"""Service: $SERVICEDESC$
108 State: $LASTSERVICESTATE$ -> $SERVICESTATE$ ($NOTIFICATIONTYPE$)
109 Command: $SERVICECHECKCOMMAND$
110 Output: $SERVICEOUTPUT$
111 Perfdata: $SERVICEPERFDATA$
112 $LONGSERVICEOUTPUT$
116 # .--helper--------------------------------------------------------------.
117 # | _ _ |
118 # | | |__ ___| |_ __ ___ _ __ |
119 # | | '_ \ / _ \ | '_ \ / _ \ '__| |
120 # | | | | | __/ | |_) | __/ | |
121 # | |_| |_|\___|_| .__/ \___|_| |
122 # | |_| |
123 # '----------------------------------------------------------------------'
126 def _transform_user_disable_notifications_opts(contact):
127 if "disable_notifications" in contact and isinstance(contact["disable_notifications"], bool):
128 return {"disable": contact["disable_notifications"]}
129 return contact.get("disable_notifications", {})
133 # .--Main----------------------------------------------------------------.
134 # | __ __ _ |
135 # | | \/ | __ _(_)_ __ |
136 # | | |\/| |/ _` | | '_ \ |
137 # | | | | | (_| | | | | | |
138 # | |_| |_|\__,_|_|_| |_| |
139 # | |
140 # +----------------------------------------------------------------------+
141 # | Main code entry point. |
142 # '----------------------------------------------------------------------'
145 def notify_usage():
146 console.error("""Usage: check_mk --notify [--keepalive]
147 check_mk --notify spoolfile <filename>
149 Normally the notify module is called without arguments to send real
150 notification. But there are situations where this module is called with
151 COMMANDS to e.g. support development of notification plugins.
153 Available commands:
154 spoolfile <filename> Reads the given spoolfile and creates a
155 notification out of its data
156 stdin Read one notification context from stdin instead
157 of taking variables from environment
158 replay N Uses the N'th recent notification from the backlog
159 and sends it again, counting from 0.
160 send-bulks Send out ripe bulk notifications
161 """)
164 # Main function called by cmk --notify. It either starts the
165 # keepalive mode (used by CMC), sends out one notifications from
166 # several possible sources or sends out all ripe bulk notifications.
167 def do_notify(options, args):
168 global _log_to_stdout, notify_mode
169 _log_to_stdout = options.get("log-to-stdout", _log_to_stdout)
171 if keepalive and "keepalive" in options:
172 keepalive.enable()
174 convert_legacy_configuration()
176 try:
177 if not os.path.exists(notification_logdir):
178 os.makedirs(notification_logdir)
179 if not os.path.exists(notification_spooldir):
180 os.makedirs(notification_spooldir)
182 notify_mode = 'notify'
183 if args:
184 notify_mode = args[0]
185 if notify_mode not in ['stdin', 'spoolfile', 'replay', 'send-bulks']:
186 console.error("ERROR: Invalid call to check_mk --notify.\n\n")
187 notify_usage()
188 sys.exit(1)
190 if len(args) != 2 and notify_mode not in ["stdin", "replay", "send-bulks"]:
191 console.error("ERROR: need an argument to --notify %s.\n\n" % notify_mode)
192 sys.exit(1)
194 elif notify_mode == 'spoolfile':
195 filename = args[1]
197 elif notify_mode == 'replay':
198 try:
199 replay_nr = int(args[1])
200 except:
201 replay_nr = 0
203 # If the notify_mode is set to 'spoolfile' we try to parse the given spoolfile
204 # This spoolfile contains a python dictionary
205 # { context: { Dictionary of environment variables }, plugin: "Plugin name" }
206 # Any problems while reading the spoolfile results in returning 2
207 # -> mknotifyd deletes this file
208 if notify_mode == "spoolfile":
209 return handle_spoolfile(filename)
211 elif keepalive and keepalive.enabled():
212 notify_keepalive()
214 elif notify_mode == 'replay':
215 raw_context = raw_context_from_backlog(replay_nr)
216 notify_notify(raw_context)
218 elif notify_mode == 'stdin':
219 notify_notify(events.raw_context_from_stdin())
221 elif notify_mode == "send-bulks":
222 send_ripe_bulks()
224 else:
225 notify_notify(raw_context_from_env())
227 except Exception:
228 crash_dir = cmk.utils.paths.var_dir + "/notify"
229 if not os.path.exists(crash_dir):
230 os.makedirs(crash_dir)
231 file(crash_dir + "/crash.log", "a").write(
232 "CRASH (%s):\n%s\n" % (time.strftime("%Y-%m-%d %H:%M:%S"), format_exception()))
235 def convert_legacy_configuration():
236 # Convert legacy spooling configuration to new one (see above)
237 if config.notification_spooling in (True, False):
238 if config.notification_spool_to:
239 also_local = config.notification_spool_to[2]
240 if also_local:
241 config.notification_spooling = "both"
242 else:
243 config.notification_spooling = "remote"
244 elif config.notification_spooling:
245 config.notification_spooling = "local"
246 else:
247 config.notification_spooling = "remote"
249 # transform deprecated value 0 to 1
250 if config.notification_logging == 0:
251 config.notification_logging = 1
254 # This function processes one raw notification and decides wether it
255 # should be spooled or not. In the latter cased a local delivery
256 # is being done.
257 def notify_notify(raw_context, analyse=False):
258 if not analyse:
259 store_notification_backlog(raw_context)
261 notify_log("----------------------------------------------------------------------")
262 if analyse:
263 notify_log("Analysing notification (%s) context with %s variables" %
264 (events.find_host_service_in_context(raw_context), len(raw_context)))
265 else:
266 notify_log("Got raw notification (%s) context with %s variables" %
267 (events.find_host_service_in_context(raw_context), len(raw_context)))
269 # Add some further variable for the conveniance of the plugins
271 if config.notification_logging >= 2:
272 notify_log(events.render_context_dump(raw_context))
274 _complete_raw_context_with_notification_vars(raw_context)
275 events.complete_raw_context(
276 raw_context, with_dump=config.notification_logging >= 2, log_func=notify_log)
278 # Spool notification to remote host, if this is enabled
279 if config.notification_spooling in ("remote", "both"):
280 create_spoolfile({"context": raw_context, "forward": True})
282 if config.notification_spooling != "remote":
283 return locally_deliver_raw_context(raw_context, analyse=analyse)
286 # Add some notification specific variables to the context. These are currently
287 # not added to alert handler scripts
288 def _complete_raw_context_with_notification_vars(raw_context):
289 raw_context["LOGDIR"] = notification_logdir
290 raw_context["MAIL_COMMAND"] = notification_mail_command
293 # Here we decide which notification implementation we are using.
294 # Hopefully we can drop a couple of them some day
295 # 1. Rule Based Notifiations (since 1.2.5i1)
296 # 2. Flexible Notifications (since 1.2.2)
297 # 3. Plain email notification (refer to git log if you are really interested)
298 def locally_deliver_raw_context(raw_context, analyse=False):
299 contactname = raw_context.get("CONTACTNAME")
300 try:
302 # If rule based notifications are enabled then the Micro Core does not set the
303 # variable CONTACTNAME. In the other cores the CONTACTNAME is being set to
304 # check-mk-notify.
305 # We do we not simply check the config variable enable_rulebased_notifications?
306 # -> Because the core needs are restart in order to reflect this while the
307 # notification mode of Check_MK not. There are thus situations where the
308 # setting of the core is different from our global variable. The core must
309 # have precedence in this situation!
310 if not contactname or contactname == "check-mk-notify":
311 # 1. RULE BASE NOTIFICATIONS
312 notify_log_debug("Preparing rule based notifications")
313 return notify_rulebased(raw_context, analyse=analyse)
315 if analyse:
316 return # Analysis only possible when rule based notifications are enabled
318 # Now fetch all configuration about that contact (it needs to be configure via
319 # Check_MK for that purpose). If we do not know that contact then we cannot use
320 # flexible notifications even if they are enabled.
321 contact = config.contacts.get(contactname)
323 disable_notifications_opts = _transform_user_disable_notifications_opts(contact)
324 if disable_notifications_opts.get("disable", False):
325 start, end = disable_notifications_opts.get("timerange", (None, None))
326 if start is None or end is None:
327 notify_log("Notifications for %s are disabled in personal settings. Skipping." %
328 contactname)
329 return
330 elif start <= time.time() <= end:
331 notify_log(
332 "Notifications for %s are disabled in personal settings from %s to %s. Skipping."
333 % (contactname, start, end))
334 return
336 # Get notification settings for the contact in question - if available.
337 if contact:
338 method = contact.get("notification_method", "email")
339 else:
340 method = "email"
342 if isinstance(method, tuple) and method[0] == 'flexible':
343 # 2. FLEXIBLE NOTIFICATIONS
344 notify_log("Preparing flexible notifications for %s" % contactname)
345 notify_flexible(raw_context, method[1])
347 else:
348 # 3. PLAIN EMAIL NOTIFICATION
349 notify_log("Preparing plain email notifications for %s" % contactname)
350 notify_plain_email(raw_context)
352 except Exception as e:
353 if cmk.utils.debug.enabled():
354 raise
355 notify_log("ERROR: %s\n%s" % (e, format_exception()))
358 def notification_replay_backlog(nr):
359 global notify_mode
360 notify_mode = "replay"
361 raw_context = raw_context_from_backlog(nr)
362 notify_notify(raw_context)
365 def notification_analyse_backlog(nr):
366 global notify_mode
367 notify_mode = "replay"
368 raw_context = raw_context_from_backlog(nr)
369 return notify_notify(raw_context, analyse=True)
373 # .--Keepalive-Mode (Used by CMC)----------------------------------------.
374 # | _ __ _ _ |
375 # | | |/ /___ ___ _ __ __ _| (_)_ _____ |
376 # | | ' // _ \/ _ \ '_ \ / _` | | \ \ / / _ \ |
377 # | | . \ __/ __/ |_) | (_| | | |\ V / __/ |
378 # | |_|\_\___|\___| .__/ \__,_|_|_| \_/ \___| |
379 # | |_| |
380 # +----------------------------------------------------------------------+
381 # | Implementation of cmk --notify --keepalive, which is being used |
382 # | by the Micro Core. |
383 # '----------------------------------------------------------------------'
386 # TODO: Make use of the generic do_keepalive() mechanism?
387 def notify_keepalive():
388 cmk_base.utils.register_sigint_handler()
389 events.event_keepalive(
390 event_function=notify_notify,
391 log_function=notify_log,
392 call_every_loop=send_ripe_bulks,
393 loop_interval=config.notification_bulk_interval,
398 # .--Rule-Based-Notifications--------------------------------------------.
399 # | ____ _ _ _ |
400 # | | _ \ _ _| | ___| |__ __ _ ___ ___ __| | |
401 # | | |_) | | | | |/ _ \ '_ \ / _` / __|/ _ \/ _` | |
402 # | | _ <| |_| | | __/ |_) | (_| \__ \ __/ (_| | |
403 # | |_| \_\\__,_|_|\___|_.__/ \__,_|___/\___|\__,_| |
404 # | |
405 # +----------------------------------------------------------------------+
406 # | Logic for rule based notifications |
407 # '----------------------------------------------------------------------'
410 def notify_rulebased(raw_context, analyse=False):
411 # First step: go through all rules and construct our table of
412 # notification plugins to call. This is a dict from (users, plugin) to
413 # a triple of (locked, parameters, bulk). If locked is True, then a user
414 # cannot cancel this notification via his personal notification rules.
415 # Example:
416 # notifications = {
417 # ( frozenset({"aa", "hh", "ll"}), "email" ) : ( False, [], None ),
418 # ( frozenset({"hh"}), "sms" ) : ( True, [ "0171737337", "bar", {
419 # 'groupby': 'host', 'interval': 60} ] ),
422 notifications = {}
423 num_rule_matches = 0
424 rule_info = []
426 for rule in config.notification_rules + user_notification_rules():
427 if "contact" in rule:
428 notify_log("User %s's rule '%s'..." % (rule["contact"], rule["description"]))
429 else:
430 notify_log("Global rule '%s'..." % rule["description"])
432 why_not = rbn_match_rule(rule, raw_context) # also checks disabling
433 if why_not:
434 notify_log(" -> does not match: %s" % why_not)
435 rule_info.append(("miss", rule, why_not))
436 else:
437 notify_log(" -> matches!")
438 num_rule_matches += 1
439 contacts = rbn_rule_contacts(rule, raw_context)
440 contactstxt = ", ".join(contacts)
442 # Handle old-style and new-style rules
443 if "notify_method" in rule: # old-style
444 plugin = rule["notify_plugin"]
445 plugin_parameters = rule[
446 "notify_method"] # None: do cancel, [ str ]: plugin parameters
447 else:
448 plugin, plugin_parameters = rule["notify_plugin"]
449 plugintxt = plugin or "plain email"
451 key = contacts, plugin
452 if plugin_parameters is None: # cancelling
453 # FIXME: In Python 2, notifications.keys() already produces a
454 # copy of the keys, while in Python 3 it is only a view of the
455 # underlying dict (modifications would result in an exception).
456 # To be explicit and future-proof, we make this hack explicit.
457 # Anyway, this is extremely ugly and an anti-patter, and it
458 # should be rewritten to something more sane.
459 for notify_key in list(notifications.keys()):
460 notify_contacts, notify_plugin = notify_key
462 overlap = notify_contacts.intersection(contacts)
463 if plugin != notify_plugin or not overlap:
464 continue
466 locked, plugin_parameters, bulk = notifications[notify_key]
468 if locked and "contact" in rule:
469 notify_log(" - cannot cancel notification of %s via %s: it is locked" %
470 (contactstxt, plugintxt))
471 continue
473 notify_log(" - cancelling notification of %s via %s" % (", ".join(overlap),
474 plugintxt))
476 remaining = notify_contacts.difference(contacts)
477 if not remaining:
478 del notifications[notify_key]
479 else:
480 new_key = remaining, plugin
481 notifications[new_key] = notifications.pop(notify_key)
482 elif contacts:
483 if key in notifications:
484 locked = notifications[key][0]
485 if locked and "contact" in rule:
486 notify_log(" - cannot modify notification of %s via %s: it is locked" %
487 (contactstxt, plugintxt))
488 continue
489 notify_log(
490 " - modifying notification of %s via %s" % (contactstxt, plugintxt))
491 else:
492 notify_log(" - adding notification of %s via %s" % (contactstxt, plugintxt))
493 bulk = rbn_get_bulk_params(rule)
494 final_parameters = rbn_finalize_plugin_parameters(raw_context["HOSTNAME"], plugin,
495 plugin_parameters)
496 notifications[key] = (not rule.get("allow_disable"), final_parameters, bulk)
498 rule_info.append(("match", rule, ""))
500 plugin_info = []
502 if not notifications:
503 if num_rule_matches:
504 notify_log("%d rules matched, but no notification has been created." % num_rule_matches)
505 elif not analyse:
506 fallback_contacts = rbn_fallback_contacts()
507 if fallback_contacts:
508 notify_log("No rule matched, notifying fallback contacts")
509 fallback_emails = [fc["email"] for fc in fallback_contacts]
510 notify_log(" Sending plain email to %s" % fallback_emails)
512 plugin_context = create_plugin_context(raw_context, [])
513 rbn_add_contact_information(plugin_context, fallback_contacts)
514 notify_via_email(plugin_context)
515 else:
516 notify_log("No rule matched, would notify fallback contacts, but none configured")
517 else:
518 # Now do the actual notifications
519 notify_log("Executing %d notifications:" % len(notifications))
520 entries = notifications.items()
521 entries.sort()
522 for (contacts, plugin), (locked, params, bulk) in entries:
523 verb = "would notify" if analyse else "notifying"
524 contactstxt = ", ".join(contacts)
525 plugintxt = plugin or "plain email"
526 paramtxt = ", ".join(params) if params else "(no parameters)"
527 bulktxt = "yes" if bulk else "no"
528 notify_log(" * %s %s via %s, parameters: %s, bulk: %s" % (verb, contactstxt, plugintxt,
529 paramtxt, bulktxt))
531 try:
532 plugin_context = create_plugin_context(raw_context, params)
533 rbn_add_contact_information(plugin_context, contacts)
535 split_contexts = (
536 plugin not in ["", "mail", "asciimail", "slack"] or
537 # params can be a list (e.g. for custom notificatios)
538 params.get("disable_multiplexing") or bulk)
539 if not split_contexts:
540 plugin_contexts = [plugin_context]
541 else:
542 plugin_contexts = rbn_split_plugin_context(plugin_context)
544 for context in plugin_contexts:
545 plugin_info.append((context["CONTACTNAME"], plugin, params, bulk))
547 if analyse:
548 continue
549 elif bulk:
550 do_bulk_notify(plugin, params, context, bulk)
551 elif config.notification_spooling in ("local", "both"):
552 create_spoolfile({"context": context, "plugin": plugin})
553 else:
554 call_notification_script(plugin, context)
556 except Exception as e:
557 if cmk.utils.debug.enabled():
558 raise
559 fe = format_exception()
560 notify_log(" ERROR: %s" % e)
561 notify_log(fe)
563 analysis_info = rule_info, plugin_info
564 return analysis_info
567 def rbn_fallback_contacts():
568 fallback_contacts = []
569 if config.notification_fallback_email:
570 fallback_contacts.append(rbn_fake_email_contact(config.notification_fallback_email))
572 for contact_name, contact in config.contacts.items():
573 if contact.get("fallback_contact", False) and contact.get("email"):
574 fallback_contact = {
575 "name": contact_name,
577 fallback_contact.update(contact)
578 fallback_contacts.append(fallback_contact)
580 return fallback_contacts
583 def rbn_finalize_plugin_parameters(hostname, plugin, rule_parameters):
584 # Right now we are only able to finalize notification plugins with dict parameters..
585 if isinstance(rule_parameters, dict):
586 parameters = config.host_extra_conf_merged(hostname,
587 config.notification_parameters.get(plugin, []))
588 parameters.update(rule_parameters)
589 return parameters
591 return rule_parameters
594 # Create a table of all user specific notification rules. Important:
595 # create deterministic order, so that rule analyses can depend on
596 # rule indices
597 def user_notification_rules():
598 user_rules = []
599 contactnames = config.contacts.keys()
600 contactnames.sort()
601 for contactname in contactnames:
602 contact = config.contacts[contactname]
603 for rule in contact.get("notification_rules", []):
604 # User notification rules always use allow_disable
605 # This line here is for legacy reasons. Newer versions
606 # already set the allow_disable option in the rule configuration
607 rule["allow_disable"] = True
609 # Save the owner of the rule for later debugging
610 rule["contact"] = contactname
611 # We assume that the "contact_..." entries in the
612 # rule are allowed and only contain one entry of the
613 # type "contact_users" : [ contactname ]. This
614 # is handled by WATO. Contact specific rules are a
615 # WATO-only feature anyway...
616 user_rules.append(rule)
618 if "authorized_sites" in config.contacts[contactname] and not "match_site" in rule:
619 rule["match_site"] = config.contacts[contactname]["authorized_sites"]
621 notify_log_debug("Found %d user specific rules" % len(user_rules))
622 return user_rules
625 def rbn_fake_email_contact(email):
626 return {
627 "name": "mailto:" + email,
628 "alias": "Explicit email adress " + email,
629 "email": email,
630 "pager": "",
634 def rbn_add_contact_information(plugin_context, contacts):
635 # TODO tb: Make contacts a reliable type. Righ now contacts can be
636 # a list of dicts or a frozenset of strings.
637 contact_dicts = []
638 keys = {"name", "alias", "email", "pager"}
640 for contact in contacts:
641 if isinstance(contact, dict):
642 contact_dict = contact
643 elif contact.startswith("mailto:"): # Fake contact
644 contact_dict = {
645 "name": contact[7:],
646 "alias": "Email address " + contact,
647 "email": contact[7:],
648 "pager": ""
650 else:
651 contact_dict = config.contacts.get(contact, {"alias": contact})
652 contact_dict["name"] = contact
654 contact_dicts.append(contact_dict)
655 keys.update([key for key in contact_dict.keys() if key.startswith("_")])
657 for key in keys:
658 context_key = "CONTACT" + key.upper()
659 items = [contact.get(key, "") for contact in contact_dicts]
660 plugin_context[context_key] = ",".join(items)
663 def rbn_split_plugin_context(plugin_context):
664 """Takes a plugin_context containing multiple contacts and returns
665 a list of plugin_contexts with a context for each contact"""
666 num_contacts = len(plugin_context["CONTACTNAME"].split(","))
667 if num_contacts <= 1:
668 return [plugin_context]
670 contexts = []
671 keys_to_split = {"CONTACTNAME", "CONTACTALIAS", "CONTACTEMAIL", "CONTACTPAGER"}
672 keys_to_split.update([key for key in plugin_context.keys() if key.startswith("CONTACT_")])
674 for i in range(num_contacts):
675 context = plugin_context.copy()
676 for key in keys_to_split:
677 context[key] = context[key].split(",")[i]
678 contexts.append(context)
680 return contexts
683 def rbn_get_bulk_params(rule):
684 bulk = rule.get("bulk")
686 if not bulk:
687 return None
688 elif isinstance(bulk, dict): # old format: treat as "Always Bulk"
689 method, params = "always", bulk
690 else:
691 method, params = bulk
693 if method == "always":
694 return params
696 elif method == "timeperiod":
697 try:
698 active = cmk_base.core.timeperiod_active(params["timeperiod"])
699 except:
700 if cmk.utils.debug.enabled():
701 raise
702 # If a livestatus connection error appears we will bulk the
703 # notification in the first place. When the connection is available
704 # again and the period is not active the notifications will be sent.
705 notify_log(" - Error checking activity of timeperiod %s: assuming active" %
706 params["timeperiod"])
707 active = True
709 if active:
710 return params
711 return params.get("bulk_outside")
713 notify_log(" - Unknown bulking method: assuming bulking is disabled")
714 return None
717 def rbn_match_rule(rule, context):
718 if rule.get("disabled"):
719 return "This rule is disabled"
721 return (events.event_match_rule(rule, context) or rbn_match_escalation(rule, context) or
722 rbn_match_escalation_throtte(rule, context) or rbn_match_host_event(rule, context) or
723 rbn_match_service_event(rule, context) or
724 rbn_match_notification_comment(rule, context) or rbn_match_event_console(rule, context))
727 def rbn_match_escalation(rule, context):
728 if "match_escalation" in rule:
729 from_number, to_number = rule["match_escalation"]
730 if context["WHAT"] == "HOST":
731 notification_number = int(context.get("HOSTNOTIFICATIONNUMBER", 1))
732 else:
733 notification_number = int(context.get("SERVICENOTIFICATIONNUMBER", 1))
734 if notification_number < from_number or notification_number > to_number:
735 return "The notification number %d does not lie in range %d ... %d" % (
736 notification_number, from_number, to_number)
739 def rbn_match_escalation_throtte(rule, context):
740 if "match_escalation_throttle" in rule:
741 # We do not want to suppress recovery notifications.
742 if (context["WHAT"] == "HOST" and context.get("HOSTSTATE", "UP") == "UP") or \
743 (context["WHAT"] == "SERVICE" and context.get("SERVICESTATE", "OK") == "OK"):
744 return
745 from_number, rate = rule["match_escalation_throttle"]
746 if context["WHAT"] == "HOST":
747 notification_number = int(context.get("HOSTNOTIFICATIONNUMBER", 1))
748 else:
749 notification_number = int(context.get("SERVICENOTIFICATIONNUMBER", 1))
750 if notification_number <= from_number:
751 return
752 if (notification_number - from_number) % rate != 0:
753 return "This notification is being skipped due to throttling. The next number will be %d" % \
754 (notification_number + rate - ((notification_number - from_number) % rate))
757 def rbn_match_host_event(rule, context):
758 if "match_host_event" in rule:
759 if context["WHAT"] != "HOST":
760 if "match_service_event" not in rule:
761 return "This is a service notification, but the rule just matches host events"
762 return # Let this be handled by match_service_event
764 allowed_events = rule["match_host_event"]
765 state = context["HOSTSTATE"]
766 last_state = context["PREVIOUSHOSTHARDSTATE"]
767 event_map = {"UP": 'r', "DOWN": 'd', "UNREACHABLE": 'u'}
768 return rbn_match_event(context, state, last_state, event_map, allowed_events)
771 def rbn_match_service_event(rule, context):
772 if "match_service_event" in rule:
773 if context["WHAT"] != "SERVICE":
774 if "match_host_event" not in rule:
775 return "This is a host notification, but the rule just matches service events"
776 return # Let this be handled by match_host_event
778 allowed_events = rule["match_service_event"]
779 state = context["SERVICESTATE"]
780 last_state = context["PREVIOUSSERVICEHARDSTATE"]
781 event_map = {"OK": 'r', "WARNING": 'w', "CRITICAL": 'c', "UNKNOWN": 'u'}
782 return rbn_match_event(context, state, last_state, event_map, allowed_events)
785 def rbn_match_event(context, state, last_state, event_map, allowed_events):
786 notification_type = context["NOTIFICATIONTYPE"]
788 if notification_type == "RECOVERY":
789 event = event_map.get(last_state, '?') + 'r'
790 elif notification_type in ["FLAPPINGSTART", "FLAPPINGSTOP", "FLAPPINGDISABLED"]:
791 event = 'f'
792 elif notification_type in ["DOWNTIMESTART", "DOWNTIMEEND", "DOWNTIMECANCELLED"]:
793 event = 's'
794 elif notification_type == "ACKNOWLEDGEMENT":
795 event = 'x'
796 elif notification_type.startswith("ALERTHANDLER ("):
797 handler_state = notification_type[14:-1]
798 if handler_state == "OK":
799 event = 'as'
800 else:
801 event = 'af'
802 else:
803 event = event_map.get(last_state, '?') + event_map.get(state, '?')
805 notify_log("Event type is %s" % event)
807 # Now go through the allowed events. Handle '?' has matching all types!
808 for allowed in allowed_events:
809 if event == allowed or \
810 (allowed[0] == '?' and len(event) > 1 and event[1] == allowed[1]) or \
811 (event[0] == '?' and len(allowed) > 1 and event[1] == allowed[1]):
812 return
814 return "Event type '%s' not handled by this rule. Allowed are: %s" % (event,
815 ", ".join(allowed_events))
818 def rbn_rule_contacts(rule, context):
819 the_contacts = set([])
820 if rule.get("contact_object"):
821 the_contacts.update(rbn_object_contact_names(context))
822 if rule.get("contact_all"):
823 the_contacts.update(rbn_all_contacts())
824 if rule.get("contact_all_with_email"):
825 the_contacts.update(rbn_all_contacts(with_email=True))
826 if "contact_users" in rule:
827 the_contacts.update(rule["contact_users"])
828 if "contact_groups" in rule:
829 the_contacts.update(rbn_groups_contacts(rule["contact_groups"]))
830 if "contact_emails" in rule:
831 the_contacts.update(rbn_emails_contacts(rule["contact_emails"]))
833 all_enabled = []
834 for contactname in the_contacts:
835 if contactname == config.notification_fallback_email:
836 contact = rbn_fake_email_contact(config.notification_fallback_email)
837 else:
838 contact = config.contacts.get(contactname)
840 if contact:
841 disable_notifications_opts = _transform_user_disable_notifications_opts(contact)
842 if disable_notifications_opts.get("disable", False):
843 start, end = disable_notifications_opts.get("timerange", (None, None))
844 if start is None or end is None:
845 notify_log(
846 " - skipping contact %s: he/she has disabled notifications" % contactname)
847 continue
848 elif start <= time.time() <= end:
849 notify_log(
850 " - skipping contact %s: he/she has disabled notifications from %s to %s."
851 % (contactname, start, end))
852 continue
854 reason = (rbn_match_contact_macros(rule, contactname, contact) or
855 rbn_match_contact_groups(rule, contactname, contact))
857 if reason:
858 notify_log(" - skipping contact %s: %s" % (contactname, reason))
859 continue
861 else:
862 notify_log("Warning: cannot get information about contact %s: ignoring restrictions" %
863 contactname)
865 all_enabled.append(contactname)
867 return frozenset(all_enabled) # has to be hashable
870 def rbn_match_contact_macros(rule, contactname, contact):
871 if "contact_match_macros" in rule:
872 for macro_name, regexp in rule["contact_match_macros"]:
873 value = contact.get("_" + macro_name, "")
874 if not regexp.endswith("$"):
875 regexp = regexp + "$"
876 if not regex(regexp).match(value):
877 macro_overview = ", ".join([
878 "%s=%s" % (varname[1:], val)
879 for (varname, val) in contact.items()
880 if varname.startswith("_")
882 return "value '%s' for macro '%s' does not match '%s'. His macros are: %s" % (
883 value, macro_name, regexp, macro_overview)
886 def rbn_match_contact_groups(rule, contactname, contact):
887 if "contact_match_groups" in rule:
888 if "contactgroups" not in contact:
889 notify_log("Warning: cannot determine contact groups of %s: skipping restrictions" %
890 contactname)
891 return
892 for required_group in rule["contact_match_groups"]:
893 if required_group not in contact["contactgroups"]:
894 return "he/she is not member of the contact group %s (his groups are %s)" % (
895 required_group, ", ".join(contact["contactgroups"] or ["<None>"]))
898 def rbn_match_notification_comment(rule, context):
899 if "match_notification_comment" in rule:
900 r = regex(rule["match_notification_comment"])
901 notification_comment = context.get("NOTIFICATIONCOMMENT", "")
902 if not r.match(notification_comment):
903 return "The beginning of the notification comment '%s' is not matched by the regex '%s'" % (
904 notification_comment, rule["match_notification_comment"])
907 def rbn_match_event_console(rule, context):
908 if "match_ec" in rule:
909 match_ec = rule["match_ec"]
910 is_ec_notification = "EC_ID" in context
911 if match_ec is False and is_ec_notification:
912 return "Notification has been created by the Event Console."
913 elif match_ec is not False and not is_ec_notification:
914 return "Notification has not been created by the Event Console."
916 if match_ec is not False:
918 # Match Event Console rule ID
919 if "match_rule_id" in match_ec and context["EC_RULE_ID"] not in match_ec[
920 "match_rule_id"]:
921 return "EC Event has rule ID '%s', but '%s' is required" % (
922 context["EC_RULE_ID"], match_ec["match_rule_id"])
924 # Match syslog priority of event
925 if "match_priority" in match_ec:
926 prio_from, prio_to = match_ec["match_priority"]
927 if prio_from > prio_to:
928 prio_to, prio_from = prio_from, prio_to
929 p = int(context["EC_PRIORITY"])
930 if p < prio_from or p > prio_to:
931 return "Event has priority %s, but matched range is %s .. %s" % (
932 p, prio_from, prio_to)
934 # Match syslog facility of event
935 if "match_facility" in match_ec:
936 if match_ec["match_facility"] != int(context["EC_FACILITY"]):
937 return "Wrong syslog facility %s, required is %s" % (context["EC_FACILITY"],
938 match_ec["match_facility"])
940 # Match event comment
941 if "match_comment" in match_ec:
942 r = regex(match_ec["match_comment"])
943 if not r.search(context["EC_COMMENT"]):
944 return "The event comment '%s' does not match the regular expression '%s'" % (
945 context["EC_COMMENT"], match_ec["match_comment"])
948 def rbn_object_contact_names(context):
949 commasepped = context.get("CONTACTS")
950 if commasepped == "?":
951 notify_log("Warning: Contacts of %s cannot be determined. Using fallback contacts" %
952 events.find_host_service_in_context(context))
953 return [contact["name"] for contact in rbn_fallback_contacts()]
954 elif commasepped:
955 return commasepped.split(",")
957 return []
960 def rbn_all_contacts(with_email=None):
961 if not with_email:
962 return config.contacts.keys() # We have that via our main.mk contact definitions!
964 return [contact_id for (contact_id, contact) in config.contacts.items() if contact.get("email")]
967 def rbn_groups_contacts(groups):
968 if not groups:
969 return {}
970 query = "GET contactgroups\nColumns: members\n"
971 for group in groups:
972 query += "Filter: name = %s\n" % group
973 query += "Or: %d\n" % len(groups)
975 try:
976 contacts = set([])
977 for contact_list in livestatus.LocalConnection().query_column(query):
978 contacts.update(contact_list)
979 return contacts
981 except livestatus.MKLivestatusNotFoundError:
982 return []
984 except Exception:
985 if cmk.utils.debug.enabled():
986 raise
987 return []
990 def rbn_emails_contacts(emails):
991 return ["mailto:" + e for e in emails]
995 # .--Flexible-Notifications----------------------------------------------.
996 # | _____ _ _ _ _ |
997 # | | ___| | _____ _(_) |__ | | ___ |
998 # | | |_ | |/ _ \ \/ / | '_ \| |/ _ \ |
999 # | | _| | | __/> <| | |_) | | __/ |
1000 # | |_| |_|\___/_/\_\_|_.__/|_|\___| |
1001 # | |
1002 # +----------------------------------------------------------------------+
1003 # | Implementation of the pre 1.2.5, hopelessly outdated flexible |
1004 # | notifications. |
1005 # '----------------------------------------------------------------------'
1008 def notify_flexible(raw_context, notification_table):
1010 for entry in notification_table:
1011 plugin = entry["plugin"]
1012 notify_log(" Notification channel with plugin %s" % (plugin or "plain email"))
1014 if not should_notify(raw_context, entry):
1015 continue
1017 plugin_context = create_plugin_context(raw_context, entry.get("parameters", []))
1019 if config.notification_spooling in ("local", "both"):
1020 create_spoolfile({"context": plugin_context, "plugin": plugin})
1021 else:
1022 call_notification_script(plugin, plugin_context)
1025 # may return
1026 # 0 : everything fine -> proceed
1027 # 1 : currently not OK -> try to process later on
1028 # >=2: invalid -> discard
1029 def should_notify(context, entry):
1030 # Check disabling
1031 if entry.get("disabled"):
1032 notify_log(" - Skipping: it is disabled for this user")
1033 return False
1035 # Check host, if configured
1036 if entry.get("only_hosts"):
1037 hostname = context.get("HOSTNAME")
1039 skip = True
1040 regex_match = False
1041 negate = False
1042 for h in entry["only_hosts"]:
1043 if h.startswith("!"): # negate
1044 negate = True
1045 h = h[1:]
1046 elif h.startswith('~'):
1047 regex_match = True
1048 h = h[1:]
1050 if not regex_match and hostname == h:
1051 skip = negate
1052 break
1054 elif regex_match and re.match(h, hostname):
1055 skip = negate
1056 break
1057 if skip:
1058 notify_log(" - Skipping: host '%s' matches none of %s" % (hostname, ", ".join(
1059 entry["only_hosts"])))
1060 return False
1062 # Check if the host has to be in a special service_level
1063 if "match_sl" in entry:
1064 from_sl, to_sl = entry['match_sl']
1065 if context['WHAT'] == "SERVICE" and context.get('SVC_SL', '').isdigit():
1066 sl = events.saveint(context.get('SVC_SL'))
1067 else:
1068 sl = events.saveint(context.get('HOST_SL'))
1070 if sl < from_sl or sl > to_sl:
1071 notify_log(" - Skipping: service level %d not between %d and %d" % (sl, from_sl, to_sl))
1072 return False
1074 # Skip blacklistet serivces
1075 if entry.get("service_blacklist"):
1076 servicedesc = context.get("SERVICEDESC")
1077 if not servicedesc:
1078 notify_log(" - Proceed: blacklist certain services, but this is a host notification")
1079 else:
1080 for s in entry["service_blacklist"]:
1081 if re.match(s, servicedesc):
1082 notify_log(" - Skipping: service '%s' matches blacklist (%s)" %
1083 (servicedesc, ", ".join(entry["service_blacklist"])))
1084 return False
1086 # Check service, if configured
1087 if entry.get("only_services"):
1088 servicedesc = context.get("SERVICEDESC")
1089 if not servicedesc:
1090 notify_log(" - Proceed: limited to certain services, but this is a host notification")
1091 else:
1092 # Example
1093 # only_services = [ "!LOG foo", "LOG", BAR" ]
1094 # -> notify all services beginning with LOG or BAR, but not "LOG foo..."
1095 skip = True
1096 for s in entry["only_services"]:
1097 if s.startswith("!"): # negate
1098 negate = True
1099 s = s[1:]
1100 else:
1101 negate = False
1102 if re.match(s, servicedesc):
1103 skip = negate
1104 break
1105 if skip:
1106 notify_log(" - Skipping: service '%s' matches none of %s" % (servicedesc, ", ".join(
1107 entry["only_services"])))
1108 return False
1110 # Check notification type
1111 event, allowed_events = check_notification_type(context, entry["host_events"],
1112 entry["service_events"])
1113 if event not in allowed_events:
1114 notify_log(" - Skipping: wrong notification type %s (%s), only %s are allowed" %
1115 (event, context["NOTIFICATIONTYPE"], ",".join(allowed_events)))
1116 return False
1118 # Check notification number (in case of repeated notifications/escalations)
1119 if "escalation" in entry:
1120 from_number, to_number = entry["escalation"]
1121 if context["WHAT"] == "HOST":
1122 notification_number = int(context.get("HOSTNOTIFICATIONNUMBER", 1))
1123 else:
1124 notification_number = int(context.get("SERVICENOTIFICATIONNUMBER", 1))
1125 if notification_number < from_number or notification_number > to_number:
1126 notify_log(" - Skipping: notification number %d does not lie in range %d ... %d" %
1127 (notification_number, from_number, to_number))
1128 return False
1130 if "timeperiod" in entry:
1131 timeperiod = entry["timeperiod"]
1132 if timeperiod and timeperiod != "24X7":
1133 if not cmk_base.core.check_timeperiod(timeperiod):
1134 notify_log(" - Skipping: time period %s is currently not active" % timeperiod)
1135 return False
1136 return True
1139 def check_notification_type(context, host_events, service_events):
1140 notification_type = context["NOTIFICATIONTYPE"]
1141 if context["WHAT"] == "HOST":
1142 allowed_events = host_events
1143 state = context["HOSTSTATE"]
1144 event_map = {"UP": 'r', "DOWN": 'd', "UNREACHABLE": 'u'}
1145 else:
1146 allowed_events = service_events
1147 state = context["SERVICESTATE"]
1148 event_map = {"OK": 'r', "WARNING": 'w', "CRITICAL": 'c', "UNKNOWN": 'u'}
1150 if notification_type == "RECOVERY":
1151 event = 'r'
1152 elif notification_type in ["FLAPPINGSTART", "FLAPPINGSTOP", "FLAPPINGDISABLED"]:
1153 event = 'f'
1154 elif notification_type in ["DOWNTIMESTART", "DOWNTIMEEND", "DOWNTIMECANCELLED"]:
1155 event = 's'
1156 elif notification_type == "ACKNOWLEDGEMENT":
1157 event = 'x'
1158 else:
1159 event = event_map.get(state, '?')
1161 return event, allowed_events
1165 # .--Plain Email---------------------------------------------------------.
1166 # | ____ _ _ _____ _ _ |
1167 # | | _ \| | __ _(_)_ __ | ____|_ __ ___ __ _(_) | |
1168 # | | |_) | |/ _` | | '_ \ | _| | '_ ` _ \ / _` | | | |
1169 # | | __/| | (_| | | | | | | |___| | | | | | (_| | | | |
1170 # | |_| |_|\__,_|_|_| |_| |_____|_| |_| |_|\__,_|_|_| |
1171 # | |
1172 # +----------------------------------------------------------------------+
1173 # | Plain Email notification, inline implemented. This is also being |
1174 # | used as a pseudo-plugin by Flexible Notification and RBN. |
1175 # '----------------------------------------------------------------------'
1178 def notify_plain_email(raw_context):
1179 plugin_context = create_plugin_context(raw_context, [])
1181 if config.notification_spooling in ("local", "both"):
1182 create_spoolfile({"context": plugin_context, "plugin": None})
1183 else:
1184 notify_log("Sending plain email to %s" % plugin_context["CONTACTNAME"])
1185 notify_via_email(plugin_context)
1188 def notify_via_email(plugin_context):
1189 notify_log(substitute_context(notification_log_template, plugin_context))
1191 if plugin_context["WHAT"] == "SERVICE":
1192 subject_t = notification_service_subject
1193 body_t = notification_service_body
1194 else:
1195 subject_t = notification_host_subject
1196 body_t = notification_host_body
1198 subject = substitute_context(subject_t, plugin_context)
1199 plugin_context["SUBJECT"] = subject
1200 body = substitute_context(notification_common_body + body_t, plugin_context)
1201 command = substitute_context(notification_mail_command, plugin_context)
1202 command_utf8 = command.encode("utf-8")
1204 # Make sure that mail(x) is using UTF-8. Otherwise we cannot send notifications
1205 # with non-ASCII characters. Unfortunately we do not know whether C.UTF-8 is
1206 # available. If e.g. mail detects a non-Ascii character in the mail body and
1207 # the specified encoding is not available, it will silently not send the mail!
1208 # Our resultion in future: use /usr/sbin/sendmail directly.
1209 # Our resultion in the present: look with locale -a for an existing UTF encoding
1210 # and use that.
1211 old_lang = os.getenv("LANG", "")
1212 for encoding in os.popen("locale -a 2>/dev/null"):
1213 l = encoding.lower()
1214 if "utf8" in l or "utf-8" in l or "utf.8" in l:
1215 encoding = encoding.strip()
1216 os.putenv("LANG", encoding)
1217 notify_log_debug("Setting locale for mail to %s." % encoding)
1218 break
1219 else:
1220 notify_log("No UTF-8 encoding found in your locale -a! Please provide C.UTF-8 encoding.")
1222 # Important: we must not output anything on stdout or stderr. Data of stdout
1223 # goes back into the socket to the CMC in keepalive mode and garbles the
1224 # handshake signal.
1225 notify_log_debug("Executing command: %s" % command)
1227 # TODO: Cleanup this shell=True call!
1228 p = subprocess.Popen( # nosec
1229 command_utf8,
1230 shell=True,
1231 stdout=subprocess.PIPE,
1232 stderr=subprocess.PIPE,
1233 stdin=subprocess.PIPE,
1234 close_fds=True)
1235 stdout_txt, stderr_txt = p.communicate(body.encode("utf-8"))
1236 exitcode = p.returncode
1237 os.putenv("LANG", old_lang) # Important: do not destroy our environment
1238 if exitcode != 0:
1239 notify_log("ERROR: could not deliver mail. Exit code of command is %r" % exitcode)
1240 for line in (stdout_txt + stderr_txt).splitlines():
1241 notify_log("mail: %s" % line.rstrip())
1242 return 2
1244 return 0
1248 # .--Plugins-------------------------------------------------------------.
1249 # | ____ _ _ |
1250 # | | _ \| |_ _ __ _(_)_ __ ___ |
1251 # | | |_) | | | | |/ _` | | '_ \/ __| |
1252 # | | __/| | |_| | (_| | | | | \__ \ |
1253 # | |_| |_|\__,_|\__, |_|_| |_|___/ |
1254 # | |___/ |
1255 # +----------------------------------------------------------------------+
1256 # | Code for the actuall calling of notification plugins (scripts). |
1257 # '----------------------------------------------------------------------'
1259 # Exit codes for plugins and also for our functions that call the plugins:
1260 # 0: Notification successfully sent
1261 # 1: Could not send now, please retry later
1262 # 2: Cannot send, retry does not make sense
1265 # Add the plugin parameters to the envinroment. We have two types of parameters:
1266 # - list, the legacy style. This will lead to PARAMETERS_1, ...
1267 # - dict, the new style for scripts with WATO rule. This will lead to
1268 # PARAMETER_FOO_BAR for a dict key named "foo_bar".
1269 def create_plugin_context(raw_context, params):
1270 plugin_context = {}
1271 plugin_context.update(raw_context) # Make a real copy
1272 events.add_to_event_context(plugin_context, "PARAMETER", params, log_function=notify_log)
1273 return plugin_context
1276 def create_bulk_parameter_context(params):
1277 dict_context = create_plugin_context({}, params)
1278 return [
1279 "%s=%s\n" % (varname, value.replace("\r", "").replace("\n", "\1"))
1280 for (varname, value) in dict_context.items()
1284 def path_to_notification_script(plugin):
1285 # Call actual script without any arguments
1286 local_path = cmk.utils.paths.local_notifications_dir + "/" + plugin
1287 if os.path.exists(local_path):
1288 path = local_path
1289 else:
1290 path = cmk.utils.paths.notifications_dir + "/" + plugin
1292 if not os.path.exists(path):
1293 notify_log("Notification plugin '%s' not found" % plugin)
1294 notify_log(" not in %s" % cmk.utils.paths.notifications_dir)
1295 notify_log(" and not in %s" % cmk.utils.paths.local_notifications_dir)
1296 return None
1298 return path
1301 # This is the function that finally sends the actual notification.
1302 # It does this by calling an external script are creating a
1303 # plain email and calling bin/mail.
1305 # It also does the central logging of the notifications
1306 # that are actually sent out.
1308 # Note: this function is *not* being called for bulk notification.
1309 def call_notification_script(plugin, plugin_context):
1310 # FIXME: This is a temporary workaround to avoid a possible crash of
1311 # subprocess.Popen when a crash dump occurs. Since in this case the
1312 # LONGSERVICEOUTPUT and LONGSERVICEOUTPUT_HTML contain very long base64
1313 # encoded strings an 'OSError: [Errno 7] Argument list too long' may
1314 # occur. To avoid this we replace the base64 encoded strings here.
1315 if plugin_context.get('LONGSERVICEOUTPUT', '').startswith('Crash dump:\\n'):
1316 plugin_context[
1317 'LONGSERVICEOUTPUT'] = u'Check failed:\\nA crash report can be submitted via the user interface.'
1318 if plugin_context.get('LONGSERVICEOUTPUT_HTML', '').startswith('Crash dump:\\n'):
1319 plugin_context[
1320 'LONGSERVICEOUTPUT_HTML'] = u'Check failed:\\nA crash report can be submitted via the user interface.'
1322 core_notification_log(plugin, plugin_context)
1324 def plugin_log(s):
1325 notify_log(" %s" % s)
1327 # The "Pseudo"-Plugin None means builtin plain email
1328 if not plugin:
1329 return notify_via_email(plugin_context)
1331 # Call actual script without any arguments
1332 path = path_to_notification_script(plugin)
1333 if not path:
1334 return 2
1336 plugin_log("executing %s" % path)
1337 try:
1338 set_notification_timeout()
1339 p = subprocess.Popen([path],
1340 stdout=subprocess.PIPE,
1341 stderr=subprocess.STDOUT,
1342 env=notification_script_env(plugin_context),
1343 close_fds=True)
1345 while True:
1346 # read and output stdout linewise to ensure we don't force python to produce
1347 # one - potentially huge - memory buffer
1348 line = p.stdout.readline()
1349 if line != '':
1350 plugin_log("Output: %s" % line.decode('utf-8').rstrip())
1351 if _log_to_stdout:
1352 console.output(line)
1353 else:
1354 break
1355 # the stdout is closed but the return code may not be available just yet - wait for the
1356 # process to actually finish
1357 exitcode = p.wait()
1358 clear_notification_timeout()
1359 except NotificationTimeout:
1360 plugin_log("Notification plugin did not finish within %d seconds. Terminating." %
1361 config.notification_plugin_timeout)
1362 # p.kill() requires python 2.6!
1363 os.kill(p.pid, signal.SIGTERM)
1364 exitcode = 1
1366 if exitcode != 0:
1367 plugin_log("Plugin exited with code %d" % exitcode)
1369 return exitcode
1372 # Construct the environment for the notification script
1373 def notification_script_env(plugin_context):
1374 return dict(os.environ.items() +
1375 [("NOTIFY_" + k, v.encode("utf-8")) for k, v in plugin_context.items()])
1378 class NotificationTimeout(Exception):
1379 pass
1382 def handle_notification_timeout(signum, frame):
1383 raise NotificationTimeout()
1386 def set_notification_timeout():
1387 signal.signal(signal.SIGALRM, handle_notification_timeout)
1388 signal.alarm(config.notification_plugin_timeout)
1391 def clear_notification_timeout():
1392 signal.alarm(0)
1396 # .--Spooling------------------------------------------------------------.
1397 # | ____ _ _ |
1398 # | / ___| _ __ ___ ___ | (_)_ __ __ _ |
1399 # | \___ \| '_ \ / _ \ / _ \| | | '_ \ / _` | |
1400 # | ___) | |_) | (_) | (_) | | | | | | (_| | |
1401 # | |____/| .__/ \___/ \___/|_|_|_| |_|\__, | |
1402 # | |_| |___/ |
1403 # +----------------------------------------------------------------------+
1404 # | Some functions dealing with the spooling of notifications. |
1405 # '----------------------------------------------------------------------'
1408 def create_spoolfile(data):
1409 if not os.path.exists(notification_spooldir):
1410 os.makedirs(notification_spooldir)
1411 file_path = "%s/%s" % (notification_spooldir, fresh_uuid())
1412 notify_log("Creating spoolfile: %s" % file_path)
1414 # First write into tempfile that is not handled by mknotifyd
1415 file(file_path + ".new", "w").write(pprint.pformat(data))
1416 os.rename(file_path + ".new", file_path)
1419 # There are three types of spool files:
1420 # 1. Notifications to be forwarded. Contain key "forward"
1421 # 2. Notifications for async local delivery. Contain key "plugin"
1422 # 3. Notifications that *were* forwarded (e.g. received from a slave). Contain neither of both.
1423 # Spool files of type 1 are not handled here!
1424 def handle_spoolfile(spoolfile):
1425 notif_uuid = spoolfile.rsplit("/", 1)[-1]
1426 notify_log("----------------------------------------------------------------------")
1427 try:
1428 data = eval(file(spoolfile).read())
1429 if "plugin" in data:
1430 plugin_context = data["context"]
1431 plugin = data["plugin"]
1432 notify_log("Got spool file %s (%s) for local delivery via %s" %
1433 (notif_uuid[:8], events.find_host_service_in_context(plugin_context),
1434 (plugin or "plain mail")))
1435 return call_notification_script(plugin, plugin_context)
1437 # We received a forwarded raw notification. We need to process
1438 # this with our local notification rules in order to call one,
1439 # several or no actual plugins.
1440 raw_context = data["context"]
1441 notify_log("Got spool file %s (%s) from remote host for local delivery." %
1442 (notif_uuid[:8], events.find_host_service_in_context(raw_context)))
1444 store_notification_backlog(data["context"])
1445 locally_deliver_raw_context(data["context"])
1446 return 0 # No error handling for async delivery
1448 except Exception as e:
1449 notify_log("ERROR %s\n%s" % (e, format_exception()))
1450 return 2
1454 # .--Bulk-Notifications--------------------------------------------------.
1455 # | ____ _ _ |
1456 # | | __ ) _ _| | | __ |
1457 # | | _ \| | | | | |/ / |
1458 # | | |_) | |_| | | < |
1459 # | |____/ \__,_|_|_|\_\ |
1460 # | |
1461 # +----------------------------------------------------------------------+
1462 # | Store postponed bulk notifications for later delivery. Deliver such |
1463 # | notifications on cmk --notify bulk. |
1464 # '----------------------------------------------------------------------'
1467 def do_bulk_notify(plugin, params, plugin_context, bulk):
1468 # First identify the bulk. The following elements identify it:
1469 # 1. contact
1470 # 2. plugin
1471 # 3. time horizon (interval) in seconds
1472 # 4. max bulked notifications
1473 # 5. elements specified in bulk["groupby"] and bulk["groupby_custom"]
1474 # We first create a bulk path constructed as a tuple of strings.
1475 # Later we convert that to a unique directory name.
1476 # Note: if you have separate bulk rules with exactly the same
1477 # bulking options, then they will use the same bulk.
1479 what = plugin_context["WHAT"]
1480 contact = plugin_context["CONTACTNAME"]
1481 if bulk.get("timeperiod"):
1482 bulk_path = (contact, plugin, 'timeperiod:' + bulk["timeperiod"], str(bulk["count"]))
1483 else:
1484 bulk_path = (contact, plugin, str(bulk["interval"]), str(bulk["count"]))
1485 bulkby = bulk["groupby"]
1487 if "bulk_subject" in bulk:
1488 plugin_context["PARAMETER_BULK_SUBJECT"] = bulk["bulk_subject"]
1490 if "host" in bulkby:
1491 bulk_path += ("host", plugin_context["HOSTNAME"])
1493 elif "folder" in bulkby:
1494 bulk_path += ("folder", find_wato_folder(plugin_context))
1496 if "service" in bulkby:
1497 bulk_path += ("service", plugin_context.get("SERVICEDESC", ""))
1499 if "sl" in bulkby:
1500 bulk_path += ("sl", plugin_context.get(what + "_SL", ""))
1502 if "check_type" in bulkby:
1503 bulk_path += ("check_type", plugin_context.get(what + "CHECKCOMMAND", "").split("!")[0])
1505 if "state" in bulkby:
1506 bulk_path += ("state", plugin_context.get(what + "STATE", ""))
1508 if "ec_contact" in bulkby:
1509 bulk_path += ("ec_contact", plugin_context.get("EC_CONTACT", ""))
1511 if "ec_comment" in bulkby:
1512 bulk_path += ("ec_comment", plugin_context.get("EC_COMMENT", ""))
1514 # User might have specified _FOO instead of FOO
1515 bulkby_custom = bulk.get("groupby_custom", [])
1516 for macroname in bulkby_custom:
1517 macroname = macroname.lstrip("_").upper()
1518 value = plugin_context.get(what + "_" + macroname, "")
1519 bulk_path += (macroname.lower(), value)
1521 notify_log(" --> storing for bulk notification %s" % "|".join(bulk_path))
1522 bulk_dirname = create_bulk_dirname(bulk_path)
1523 uuid = fresh_uuid()
1524 filename = bulk_dirname + "/" + uuid
1525 file(filename + ".new", "w").write("%r\n" % ((params, plugin_context),))
1526 os.rename(filename + ".new", filename) # We need an atomic creation!
1527 notify_log(" - stored in %s" % filename)
1530 def find_wato_folder(context):
1531 for tag in context.get("HOSTTAGS", "").split():
1532 if tag.startswith("/wato/"):
1533 return tag[6:].rstrip("/")
1534 return ""
1537 def create_bulk_dirname(bulk_path):
1538 dirname = os.path.join(notification_bulkdir, bulk_path[0], bulk_path[1],
1539 ",".join([b.replace("/", "\\") for b in bulk_path[2:]]))
1541 # Remove non-Ascii-characters by special %02x-syntax
1542 try:
1543 str(dirname)
1544 except:
1545 new_dirname = ""
1546 for char in dirname:
1547 if ord(char) <= 0 or ord(char) > 127:
1548 new_dirname += "%%%04x" % ord(char)
1549 else:
1550 new_dirname += char
1551 dirname = new_dirname
1553 if not os.path.exists(dirname):
1554 os.makedirs(dirname)
1555 notify_log(" - created bulk directory %s" % dirname)
1556 return dirname
1559 def bulk_parts(method_dir, bulk):
1560 parts = bulk.split(',')
1562 try:
1563 interval, timeperiod = int(parts[0]), None
1564 except ValueError:
1565 entry = parts[0].split(':')
1566 if entry[0] == 'timeperiod' and len(entry) == 2:
1567 interval, timeperiod = None, entry[1]
1568 else:
1569 notify_log("Skipping invalid bulk directory %s" % method_dir)
1570 return None
1572 try:
1573 count = int(parts[1])
1574 except ValueError:
1575 notify_log("Skipping invalid bulk directory %s" % method_dir)
1576 return None
1578 return interval, timeperiod, count
1581 def bulk_uuids(bulk_dir):
1582 uuids, oldest = [], time.time()
1583 for uuid in os.listdir(bulk_dir): # 4ded0fa2-f0cd-4b6a-9812-54374a04069f
1584 if uuid.endswith(".new"):
1585 continue
1586 if len(uuid) != 36:
1587 notify_log("Skipping invalid notification file %s" % os.path.join(bulk_dir, uuid))
1588 continue
1590 mtime = os.stat(os.path.join(bulk_dir, uuid)).st_mtime
1591 uuids.append((mtime, uuid))
1592 oldest = min(oldest, mtime)
1593 uuids.sort()
1594 return uuids, oldest
1597 def remove_if_orphaned(bulk_dir, max_age, ref_time=None):
1598 if not ref_time:
1599 ref_time = time.time()
1601 dirage = ref_time - os.stat(bulk_dir).st_mtime
1602 if dirage > max_age:
1603 notify_log("Warning: removing orphaned empty bulk directory %s" % bulk_dir)
1604 try:
1605 os.rmdir(bulk_dir)
1606 except Exception as e:
1607 notify_log(" -> Error removing it: %s" % e)
1610 def find_bulks(only_ripe):
1611 if not os.path.exists(notification_bulkdir):
1612 return []
1614 def listdir_visible(path):
1615 return [x for x in os.listdir(path) if not x.startswith(".")]
1617 bulks, now = [], time.time()
1618 for contact in listdir_visible(notification_bulkdir):
1619 contact_dir = os.path.join(notification_bulkdir, contact)
1620 for method in listdir_visible(contact_dir):
1621 method_dir = os.path.join(contact_dir, method)
1622 for bulk in listdir_visible(method_dir):
1623 bulk_dir = os.path.join(method_dir, bulk)
1625 uuids, oldest = bulk_uuids(bulk_dir)
1626 if not uuids:
1627 remove_if_orphaned(bulk_dir, max_age=60, ref_time=now)
1628 continue
1629 age = now - oldest
1631 # e.g. 60,10,host,localhost OR timeperiod:late_night,1000,host,localhost
1632 parts = bulk_parts(method_dir, bulk)
1633 if not parts:
1634 continue
1635 interval, timeperiod, count = parts
1637 if interval is not None:
1638 if age >= interval:
1639 notify_log("Bulk %s is ripe: age %d >= %d" % (bulk_dir, age, interval))
1640 elif len(uuids) >= count:
1641 notify_log(
1642 "Bulk %s is ripe: count %d >= %d" % (bulk_dir, len(uuids), count))
1643 else:
1644 notify_log("Bulk %s is not ripe yet (age: %d, count: %d)!" % (bulk_dir, age,
1645 len(uuids)))
1646 if only_ripe:
1647 continue
1649 bulks.append((bulk_dir, age, interval, 'n.a.', count, uuids))
1650 else:
1651 try:
1652 active = cmk_base.core.timeperiod_active(timeperiod)
1653 except:
1654 # This prevents sending bulk notifications if a
1655 # livestatus connection error appears. It also implies
1656 # that an ongoing connection error will hold back bulk
1657 # notifications.
1658 notify_log("Error while checking activity of timeperiod %s: assuming active"
1659 % timeperiod)
1660 active = True
1662 if active is True and len(uuids) < count:
1663 # Only add a log entry every 10 minutes since timeperiods
1664 # can be very long (The default would be 10s).
1665 if now % 600 <= config.notification_bulk_interval:
1666 notify_log("Bulk %s is not ripe yet (timeperiod %s: active, count: %d)"
1667 % (bulk_dir, timeperiod, len(uuids)))
1669 if only_ripe:
1670 continue
1671 elif active is False:
1672 notify_log(
1673 "Bulk %s is ripe: timeperiod %s has ended" % (bulk_dir, timeperiod))
1674 elif len(uuids) >= count:
1675 notify_log(
1676 "Bulk %s is ripe: count %d >= %d" % (bulk_dir, len(uuids), count))
1677 else:
1678 notify_log("Bulk %s is ripe: timeperiod %s is not known anymore" %
1679 (bulk_dir, timeperiod))
1681 bulks.append((bulk_dir, age, 'n.a.', timeperiod, count, uuids))
1683 return bulks
1686 def send_ripe_bulks():
1687 ripe = find_bulks(True)
1688 if ripe:
1689 notify_log("Sending out %d ripe bulk notifications" % len(ripe))
1690 for bulk in ripe:
1691 try:
1692 notify_bulk(bulk[0], bulk[-1])
1693 except Exception:
1694 if cmk.utils.debug.enabled():
1695 raise
1696 notify_log("Error sending bulk %s: %s" % (bulk[0], format_exception()))
1699 def notify_bulk(dirname, uuids):
1700 parts = dirname.split("/")
1701 contact = parts[-3]
1702 plugin = parts[-2]
1703 notify_log(" -> %s/%s %s" % (contact, plugin, dirname))
1704 # If new entries are created in this directory while we are working
1705 # on it, nothing bad happens. It's just that we cannot remove
1706 # the directory after our work. It will be the starting point for
1707 # the next bulk with the same ID, which is completely OK.
1708 bulk_context = []
1709 old_params = None
1710 unhandled_uuids = []
1711 for mtime, uuid in uuids:
1712 try:
1713 params, context = eval(file(dirname + "/" + uuid).read())
1714 except Exception as e:
1715 if cmk.utils.debug.enabled():
1716 raise
1717 notify_log(" Deleting corrupted or empty bulk file %s/%s: %s" % (dirname, uuid, e))
1718 continue
1720 if old_params is None:
1721 old_params = params
1722 elif params != old_params:
1723 notify_log(" Parameters are different from previous, postponing into separate bulk")
1724 unhandled_uuids.append((mtime, uuid))
1725 continue
1727 part_block = []
1728 part_block.append("\n")
1729 for varname, value in context.items():
1730 part_block.append("%s=%s\n" % (varname, value.replace("\r", "").replace("\n", "\1")))
1731 bulk_context.append(part_block)
1733 # Do not forget to add this to the monitoring log. We create
1734 # a single entry for each notification contained in the bulk.
1735 # It is important later to have this precise information.
1736 plugin_name = "bulk " + (plugin or "plain email")
1737 core_notification_log(plugin_name, context)
1739 if bulk_context: # otherwise: only corrupted files
1740 # Per default the uuids are sorted chronologically from oldest to newest
1741 # Therefore the notification plugin also shows the oldest entry first
1742 # The following configuration option allows to reverse the sorting
1743 if isinstance(old_params, dict) and old_params.get("bulk_sort_order") == "newest_first":
1744 bulk_context.reverse()
1746 # Converts bulk context from [[1,2],[3,4]] to [1,2,3,4]
1747 bulk_context = [x for y in bulk_context for x in y]
1749 parameter_context = create_bulk_parameter_context(old_params)
1750 context_text = "".join(parameter_context + bulk_context)
1751 call_bulk_notification_script(plugin, context_text)
1752 else:
1753 notify_log("No valid notification file left. Skipping this bulk.")
1755 # Remove sent notifications
1756 for mtime, uuid in uuids:
1757 if (mtime, uuid) not in unhandled_uuids:
1758 path = os.path.join(dirname, uuid)
1759 try:
1760 os.remove(path)
1761 except Exception as e:
1762 notify_log("Cannot remove %s: %s" % (path, e))
1764 # Repeat with unhandled uuids (due to different parameters)
1765 if unhandled_uuids:
1766 notify_bulk(dirname, unhandled_uuids)
1768 # Remove directory. Not neccessary if emtpy
1769 try:
1770 os.rmdir(dirname)
1771 except Exception as e:
1772 if not unhandled_uuids:
1773 notify_log("Warning: cannot remove directory %s: %s" % (dirname, e))
1776 def call_bulk_notification_script(plugin, context_text):
1777 path = path_to_notification_script(plugin)
1778 if not path:
1779 raise MKGeneralException("Notification plugin %s not found" % plugin)
1781 stdout_txt = stderr_txt = ""
1782 try:
1783 set_notification_timeout()
1785 # Protocol: The script gets the context on standard input and
1786 # read until that is closed. It is being called with the parameter
1787 # --bulk.
1788 p = subprocess.Popen([path, "--bulk"],
1789 stdout=subprocess.PIPE,
1790 stderr=subprocess.PIPE,
1791 stdin=subprocess.PIPE,
1792 close_fds=True)
1794 stdout_txt, stderr_txt = p.communicate(context_text.encode("utf-8"))
1795 exitcode = p.returncode
1797 clear_notification_timeout()
1798 except NotificationTimeout:
1799 notify_log("Notification plugin did not finish within %d seconds. Terminating." %
1800 config.notification_plugin_timeout)
1801 # p.kill() requires python 2.6!
1802 os.kill(p.pid, signal.SIGTERM)
1803 exitcode = 1
1805 if exitcode:
1806 notify_log("ERROR: script %s --bulk returned with exit code %s" % (path, exitcode))
1807 for line in (stdout_txt + stderr_txt).splitlines():
1808 notify_log("%s: %s" % (plugin, line.rstrip()))
1812 # .--Contexts------------------------------------------------------------.
1813 # | ____ _ _ |
1814 # | / ___|___ _ __ | |_ _____ _| |_ ___ |
1815 # | | | / _ \| '_ \| __/ _ \ \/ / __/ __| |
1816 # | | |__| (_) | | | | || __/> <| |_\__ \ |
1817 # | \____\___/|_| |_|\__\___/_/\_\\__|___/ |
1818 # | |
1819 # +----------------------------------------------------------------------+
1820 # | Functions dealing with loading, storing and converting contexts. |
1821 # '----------------------------------------------------------------------'
1824 # Be aware: The backlog.mk contains the raw context which has not been decoded
1825 # to unicode yet. It contains raw encoded strings e.g. the plugin output provided
1826 # by third party plugins which might be UTF-8 encoded but can also be encoded in
1827 # other ways. Currently the context is converted later by bot, this module
1828 # and the GUI. TODO Maybe we should centralize the encoding here and save the
1829 # backlock already encoded.
1830 def store_notification_backlog(raw_context):
1831 path = notification_logdir + "/backlog.mk"
1832 if not config.notification_backlog:
1833 if os.path.exists(path):
1834 os.remove(path)
1835 return
1837 try:
1838 backlog = eval(file(path).read())[:config.notification_backlog - 1]
1839 except:
1840 backlog = []
1842 backlog = [raw_context] + backlog
1843 file(path, "w").write("%r\n" % backlog)
1846 def raw_context_from_backlog(nr):
1847 try:
1848 backlog = eval(file(notification_logdir + "/backlog.mk").read())
1849 except:
1850 backlog = []
1852 if nr < 0 or nr >= len(backlog):
1853 console.error("No notification number %d in backlog.\n" % nr)
1854 sys.exit(2)
1856 notify_log("Replaying notification %d from backlog...\n" % nr)
1857 return backlog[nr]
1860 def raw_context_from_env():
1861 # Information about notification is excpected in the
1862 # environment in variables with the prefix NOTIFY_
1863 return dict([(var[7:], value)
1864 for (var, value) in os.environ.items()
1865 if var.startswith("NOTIFY_") and not dead_nagios_variable(value)])
1868 def substitute_context(template, context):
1869 # First replace all known variables
1870 for varname, value in context.items():
1871 template = template.replace('$' + varname + '$', value)
1873 # Remove the rest of the variables and make them empty
1874 template = re.sub(r"\$[A-Z]+\$", "", template)
1875 return template
1879 # .--Helpers-------------------------------------------------------------.
1880 # | _ _ _ |
1881 # | | | | | ___| |_ __ ___ _ __ ___ |
1882 # | | |_| |/ _ \ | '_ \ / _ \ '__/ __| |
1883 # | | _ | __/ | |_) | __/ | \__ \ |
1884 # | |_| |_|\___|_| .__/ \___|_| |___/ |
1885 # | |_| |
1886 # +----------------------------------------------------------------------+
1887 # | Some generic helper functions |
1888 # '----------------------------------------------------------------------'
1891 def format_exception():
1892 import traceback
1893 import StringIO
1895 txt = StringIO.StringIO()
1896 t, v, tb = sys.exc_info()
1897 traceback.print_exception(t, v, tb, None, txt)
1898 return txt.getvalue()
1901 def dead_nagios_variable(value):
1902 if len(value) < 3:
1903 return False
1904 if value[0] != '$' or value[-1] != '$':
1905 return False
1906 for c in value[1:-1]:
1907 if not c.isupper() and c != '_':
1908 return False
1909 return True
1912 def notify_log(message):
1913 if config.notification_logging >= 1:
1914 events.event_log(notification_log, message)
1917 def notify_log_debug(message):
1918 if config.notification_logging >= 2:
1919 notify_log(message)
1922 def fresh_uuid():
1923 try:
1924 return file('/proc/sys/kernel/random/uuid').read().strip()
1925 except IOError:
1926 # On platforms where the above file does not exist we try to
1927 # use the python uuid module which seems to be a good fallback
1928 # for those systems. Well, if got python < 2.5 you are lost for now.
1929 import uuid
1930 return str(uuid.uuid4())
1933 def core_notification_log(plugin, plugin_context):
1934 what = plugin_context["WHAT"]
1935 contact = plugin_context["CONTACTNAME"]
1936 spec = plugin_context["HOSTNAME"]
1937 if what == "HOST":
1938 state = plugin_context["HOSTSTATE"]
1939 output = plugin_context["HOSTOUTPUT"]
1940 if what == "SERVICE":
1941 spec += ";" + plugin_context["SERVICEDESC"]
1942 state = plugin_context["SERVICESTATE"]
1943 output = plugin_context["SERVICEOUTPUT"]
1945 log_message = "%s NOTIFICATION: %s;%s;%s;%s;%s" % (what, contact, spec, state, plugin or
1946 "plain email", output)
1948 _send_livestatus_command("LOG;%s" % log_message)
1951 def _send_livestatus_command(command):
1952 try:
1953 livestatus.LocalConnection().command("[%d] %s" % (time.time(), command.encode("utf-8")))
1954 except Exception as e:
1955 if cmk.utils.debug.enabled():
1956 raise
1957 notify_log("WARNING: cannot send livestatus command: %s" % e)
1958 notify_log("Command was: %s" % command)