2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
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.
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
56 import cmk_base
.config
as config
57 import cmk_base
.console
as console
59 import cmk_base
.events
as events
62 import cmk_base
.cee
.keepalive
as keepalive
64 keepalive
= None # type: ignore
66 _log_to_stdout
= False
67 notify_mode
= "notify"
69 # .--Configuration-------------------------------------------------------.
71 # | / ___|___ _ __ / _(_) __ _ _ _ _ __ __ _| |_(_) ___ _ __ |
72 # | | | / _ \| '_ \| |_| |/ _` | | | | '__/ _` | __| |/ _ \| '_ \ |
73 # | | |__| (_) | | | | _| | (_| | |_| | | | (_| | |_| | (_) | | | | |
74 # | \____\___/|_| |_|_| |_|\__, |\__,_|_| \__,_|\__|_|\___/|_| |_| |
76 # +----------------------------------------------------------------------+
77 # | Default values of global configuration variables. |
78 # '----------------------------------------------------------------------'
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$
98 Address: $HOSTADDRESS$
101 notification_host_body
= u
"""State: $LASTHOSTSTATE$ -> $HOSTSTATE$ ($NOTIFICATIONTYPE$)
102 Command: $HOSTCHECKCOMMAND$
104 Perfdata: $HOSTPERFDATA$
108 notification_service_body
= u
"""Service: $SERVICEDESC$
109 State: $LASTSERVICESTATE$ -> $SERVICESTATE$ ($NOTIFICATIONTYPE$)
110 Command: $SERVICECHECKCOMMAND$
111 Output: $SERVICEOUTPUT$
112 Perfdata: $SERVICEPERFDATA$
117 # .--helper--------------------------------------------------------------.
119 # | | |__ ___| |_ __ ___ _ __ |
120 # | | '_ \ / _ \ | '_ \ / _ \ '__| |
121 # | | | | | __/ | |_) | __/ | |
122 # | |_| |_|\___|_| .__/ \___|_| |
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----------------------------------------------------------------.
136 # | | \/ | __ _(_)_ __ |
137 # | | |\/| |/ _` | | '_ \ |
138 # | | | | | (_| | | | | | |
139 # | |_| |_|\__,_|_|_| |_| |
141 # +----------------------------------------------------------------------+
142 # | Main code entry point. |
143 # '----------------------------------------------------------------------'
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.
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
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
:
175 convert_legacy_configuration()
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'
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")
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
)
195 elif notify_mode
== 'spoolfile':
198 elif notify_mode
== 'replay':
200 replay_nr
= int(args
[1])
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():
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":
226 notify_notify(raw_context_from_env())
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]
242 config
.notification_spooling
= "both"
244 config
.notification_spooling
= "remote"
245 elif config
.notification_spooling
:
246 config
.notification_spooling
= "local"
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
262 def notify_notify(raw_context
, analyse
=False):
264 store_notification_backlog(raw_context
)
266 notify_log("----------------------------------------------------------------------")
268 notify_log("Analysing notification (%s) context with %s variables" %
269 (events
.find_host_service_in_context(raw_context
), len(raw_context
)))
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")
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
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
)
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." %
334 elif start
<= time
.time() <= end
:
336 "Notifications for %s are disabled in personal settings from %s to %s. Skipping."
337 % (contactname
, start
, end
))
340 # Get notification settings for the contact in question - if available.
342 method
= contact
.get("notification_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])
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():
359 notify_log("ERROR: %s\n%s" % (e
, format_exception()))
362 def notification_replay_backlog(nr
):
364 notify_mode
= "replay"
365 raw_context
= raw_context_from_backlog(nr
)
366 notify_notify(raw_context
)
369 def notification_analyse_backlog(nr
):
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)----------------------------------------.
379 # | | |/ /___ ___ _ __ __ _| (_)_ _____ |
380 # | | ' // _ \/ _ \ '_ \ / _` | | \ \ / / _ \ |
381 # | | . \ __/ __/ |_) | (_| | | |\ V / __/ |
382 # | |_|\_\___|\___| .__/ \__,_|_|_| \_/ \___| |
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--------------------------------------------.
404 # | | _ \ _ _| | ___| |__ __ _ ___ ___ __| | |
405 # | | |_) | | | | |/ _ \ '_ \ / _` / __|/ _ \/ _` | |
406 # | | _ <| |_| | | __/ |_) | (_| \__ \ __/ (_| | |
407 # | |_| \_\\__,_|_|\___|_.__/ \__,_|___/\___|\__,_| |
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.
421 # ( frozenset({"aa", "hh", "ll"}), "email" ) : ( False, [], None ),
422 # ( frozenset({"hh"}), "sms" ) : ( True, [ "0171737337", "bar", {
423 # 'groupby': 'host', 'interval': 60} ] ),
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"])
434 contact_info
= "Global rule '%s'..." % rule
["description"]
436 why_not
= rbn_match_rule(rule
, raw_context
) # also checks disabling
438 notify_log_verbose(contact_info
)
439 notify_log_verbose(" -> does not match: %s" % why_not
)
440 rule_info
.append(("miss", rule
, why_not
))
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
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
:
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
))
479 notify_log(" - cancelling notification of %s via %s" % (", ".join(overlap
),
482 remaining
= notify_contacts
.difference(contacts
)
484 del notifications
[notify_key
]
486 new_key
= remaining
, plugin
487 notifications
[new_key
] = notifications
.pop(notify_key
)
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
))
496 " - modifying notification of %s via %s" % (contactstxt
, plugintxt
))
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
,
502 notifications
[key
] = (not rule
.get("allow_disable"), final_parameters
, bulk
)
504 rule_info
.append(("match", rule
, ""))
508 if not notifications
:
510 notify_log("%d rules matched, but no notification has been created." % num_rule_matches
)
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
)
522 notify_log("No rule matched, would notify fallback contacts, but none configured")
524 # Now do the actual notifications
525 notify_log("Executing %d notifications:" % len(notifications
))
526 entries
= notifications
.items()
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
,
538 plugin_context
= create_plugin_context(raw_context
, params
)
539 rbn_add_contact_information(plugin_context
, contacts
)
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
]
548 plugin_contexts
= rbn_split_plugin_context(plugin_context
)
550 for context
in plugin_contexts
:
551 plugin_info
.append((context
["CONTACTNAME"], plugin
, params
, bulk
))
556 do_bulk_notify(plugin
, params
, context
, bulk
)
557 elif config
.notification_spooling
in ("local", "both"):
558 create_spoolfile({"context": context
, "plugin": plugin
})
560 call_notification_script(plugin
, context
)
562 except Exception as e
:
563 if cmk
.utils
.debug
.enabled():
565 fe
= format_exception()
566 notify_log(" ERROR: %s" % e
)
569 analysis_info
= rule_info
, plugin_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"):
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
)
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
603 def user_notification_rules():
605 contactnames
= config
.contacts
.keys()
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
))
631 def rbn_fake_email_contact(email
):
633 "name": "mailto:" + email
,
634 "alias": "Explicit email adress " + email
,
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.
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
652 "alias": "Email address " + contact
,
653 "email": contact
[7:],
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("_")])
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
]
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
)
689 def rbn_get_bulk_params(rule
):
690 bulk
= rule
.get("bulk")
694 elif isinstance(bulk
, dict): # old format: treat as "Always Bulk"
695 method
, params
= "always", bulk
697 method
, params
= bulk
699 if method
== "always":
702 elif method
== "timeperiod":
704 active
= cmk_base
.core
.timeperiod_active(params
["timeperiod"])
706 if cmk
.utils
.debug
.enabled():
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"])
717 return params
.get("bulk_outside")
719 notify_log(" - Unknown bulking method: assuming bulking is disabled")
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))
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"):
751 from_number
, rate
= rule
["match_escalation_throttle"]
752 if context
["WHAT"] == "HOST":
753 notification_number
= int(context
.get("HOSTNOTIFICATIONNUMBER", 1))
755 notification_number
= int(context
.get("SERVICENOTIFICATIONNUMBER", 1))
756 if notification_number
<= from_number
:
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"]:
798 elif notification_type
in ["DOWNTIMESTART", "DOWNTIMEEND", "DOWNTIMECANCELLED"]:
800 elif notification_type
== "ACKNOWLEDGEMENT":
802 elif notification_type
.startswith("ALERTHANDLER ("):
803 handler_state
= notification_type
[14:-1]
804 if handler_state
== "OK":
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]):
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"]))
838 for contactname
in the_contacts
:
839 if contactname
== config
.notification_fallback_email
:
840 contact
= rbn_fake_email_contact(config
.notification_fallback_email
)
842 contact
= config
.contacts
.get(contactname
)
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:
850 " - skipping contact %s: he/she has disabled notifications" % contactname
)
852 elif start
<= time
.time() <= end
:
854 " - skipping contact %s: he/she has disabled notifications from %s to %s."
855 % (contactname
, start
, end
))
858 reason
= (rbn_match_contact_macros(rule
, contactname
, contact
) or
859 rbn_match_contact_groups(rule
, contactname
, contact
))
862 notify_log(" - skipping contact %s: %s" % (contactname
, reason
))
866 notify_log("Warning: cannot get information about contact %s: ignoring restrictions" %
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" %
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
[
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()]
959 return commasepped
.split(",")
964 def rbn_all_contacts(with_email
=None):
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
):
974 query
= "GET contactgroups\nColumns: members\n"
976 query
+= "Filter: name = %s\n" % group
977 query
+= "Or: %d\n" % len(groups
)
981 for contact_list
in livestatus
.LocalConnection().query_column(query
):
982 contacts
.update(contact_list
)
985 except livestatus
.MKLivestatusNotFoundError
:
989 if cmk
.utils
.debug
.enabled():
994 def rbn_emails_contacts(emails
):
995 return ["mailto:" + e
for e
in emails
]
999 # .--Flexible-Notifications----------------------------------------------.
1001 # | | ___| | _____ _(_) |__ | | ___ |
1002 # | | |_ | |/ _ \ \/ / | '_ \| |/ _ \ |
1003 # | | _| | | __/> <| | |_) | | __/ |
1004 # | |_| |_|\___/_/\_\_|_.__/|_|\___| |
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
):
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
})
1026 call_notification_script(plugin
, plugin_context
)
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
):
1035 if entry
.get("disabled"):
1036 notify_log(" - Skipping: it is disabled for this user")
1039 # Check host, if configured
1040 if entry
.get("only_hosts"):
1041 hostname
= context
.get("HOSTNAME")
1046 for h
in entry
["only_hosts"]:
1047 if h
.startswith("!"): # negate
1050 elif h
.startswith('~'):
1054 if not regex_match
and hostname
== h
:
1058 elif regex_match
and re
.match(h
, hostname
):
1062 notify_log(" - Skipping: host '%s' matches none of %s" % (hostname
, ", ".join(
1063 entry
["only_hosts"])))
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'))
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
))
1078 # Skip blacklistet serivces
1079 if entry
.get("service_blacklist"):
1080 servicedesc
= context
.get("SERVICEDESC")
1082 notify_log(" - Proceed: blacklist certain services, but this is a host notification")
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"])))
1090 # Check service, if configured
1091 if entry
.get("only_services"):
1092 servicedesc
= context
.get("SERVICEDESC")
1094 notify_log(" - Proceed: limited to certain services, but this is a host notification")
1097 # only_services = [ "!LOG foo", "LOG", BAR" ]
1098 # -> notify all services beginning with LOG or BAR, but not "LOG foo..."
1100 for s
in entry
["only_services"]:
1101 if s
.startswith("!"): # negate
1106 if re
.match(s
, servicedesc
):
1110 notify_log(" - Skipping: service '%s' matches none of %s" % (servicedesc
, ", ".join(
1111 entry
["only_services"])))
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
)))
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))
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
))
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
)
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'}
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":
1156 elif notification_type
in ["FLAPPINGSTART", "FLAPPINGSTOP", "FLAPPINGDISABLED"]:
1158 elif notification_type
in ["DOWNTIMESTART", "DOWNTIMEEND", "DOWNTIMECANCELLED"]:
1160 elif notification_type
== "ACKNOWLEDGEMENT":
1163 event
= event_map
.get(state
, '?')
1165 return event
, allowed_events
1169 # .--Plain Email---------------------------------------------------------.
1170 # | ____ _ _ _____ _ _ |
1171 # | | _ \| | __ _(_)_ __ | ____|_ __ ___ __ _(_) | |
1172 # | | |_) | |/ _` | | '_ \ | _| | '_ ` _ \ / _` | | | |
1173 # | | __/| | (_| | | | | | | |___| | | | | | (_| | | | |
1174 # | |_| |_|\__,_|_|_| |_| |_____|_| |_| |_|\__,_|_|_| |
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})
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
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
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
)
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
1229 notify_log_debug("Executing command: %s" % command
)
1231 # TODO: Cleanup this shell=True call!
1232 p
= subprocess
.Popen( # nosec
1235 stdout
=subprocess
.PIPE
,
1236 stderr
=subprocess
.PIPE
,
1237 stdin
=subprocess
.PIPE
,
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
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())
1252 # .--Plugins-------------------------------------------------------------.
1254 # | | _ \| |_ _ __ _(_)_ __ ___ |
1255 # | | |_) | | | | |/ _` | | '_ \/ __| |
1256 # | | __/| | |_| | (_| | | | | \__ \ |
1257 # | |_| |_|\__,_|\__, |_|_| |_|___/ |
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
):
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
)
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
):
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
)
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
))
1317 notify_log(" %s" % s
)
1319 # The "Pseudo"-Plugin None means builtin plain email
1321 return notify_via_email(plugin_context
)
1323 # Call actual script without any arguments
1324 path
= path_to_notification_script(plugin
)
1328 plugin_log("executing %s" % path
)
1330 set_notification_timeout()
1331 p
= subprocess
.Popen([path
],
1332 stdout
=subprocess
.PIPE
,
1333 stderr
=subprocess
.STDOUT
,
1334 env
=notification_script_env(plugin_context
),
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()
1342 plugin_log("Output: %s" % line
.decode('utf-8').rstrip())
1344 console
.output(line
)
1347 # the stdout is closed but the return code may not be available just yet - wait for the
1348 # process to actually finish
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
)
1359 plugin_log("Plugin exited with code %d" % 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
1373 max_length
= 32 * os
.sysconf("SC_PAGESIZE") // 2
1375 max_length
= 32 * 4046 // 2
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")
1383 "NOTIFY_" + variable
: format_(value
) for variable
, value
in plugin_context
.iteritems()
1385 notify_env
.update(os
.environ
)
1389 class NotificationTimeout(Exception):
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():
1407 # .--Spooling------------------------------------------------------------.
1409 # | / ___| _ __ ___ ___ | (_)_ __ __ _ |
1410 # | \___ \| '_ \ / _ \ / _ \| | | '_ \ / _` | |
1411 # | ___) | |_) | (_) | (_) | | | | | | (_| | |
1412 # | |____/| .__/ \___/ \___/|_|_|_| |_|\__, | |
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("----------------------------------------------------------------------")
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()))
1465 # .--Bulk-Notifications--------------------------------------------------.
1467 # | | __ ) _ _| | | __ |
1468 # | | _ \| | | | | |/ / |
1469 # | | |_) | |_| | | < |
1470 # | |____/ \__,_|_|_|\_\ |
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:
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"]))
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", ""))
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
)
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("/")
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
1557 for char
in dirname
:
1558 if ord(char
) <= 0 or ord(char
) > 127:
1559 new_dirname
+= "%%%04x" % ord(char
)
1562 dirname
= new_dirname
1564 if not os
.path
.exists(dirname
):
1565 os
.makedirs(dirname
)
1566 notify_log(" - created bulk directory %s" % dirname
)
1570 def bulk_parts(method_dir
, bulk
):
1571 parts
= bulk
.split(',')
1574 interval
, timeperiod
= int(parts
[0]), None
1576 entry
= parts
[0].split(':')
1577 if entry
[0] == 'timeperiod' and len(entry
) == 2:
1578 interval
, timeperiod
= None, entry
[1]
1580 notify_log("Skipping invalid bulk directory %s" % method_dir
)
1584 count
= int(parts
[1])
1586 notify_log("Skipping invalid bulk directory %s" % method_dir
)
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"):
1598 notify_log("Skipping invalid notification file %s" % os
.path
.join(bulk_dir
, uuid
))
1601 mtime
= os
.stat(os
.path
.join(bulk_dir
, uuid
)).st_mtime
1602 uuids
.append((mtime
, uuid
))
1603 oldest
= min(oldest
, mtime
)
1605 return uuids
, oldest
1608 def remove_if_orphaned(bulk_dir
, max_age
, ref_time
=None):
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
)
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
):
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
)
1638 remove_if_orphaned(bulk_dir
, max_age
=60, ref_time
=now
)
1642 # e.g. 60,10,host,localhost OR timeperiod:late_night,1000,host,localhost
1643 parts
= bulk_parts(method_dir
, bulk
)
1646 interval
, timeperiod
, count
= parts
1648 if interval
is not None:
1650 notify_log("Bulk %s is ripe: age %d >= %d" % (bulk_dir
, age
, interval
))
1651 elif len(uuids
) >= count
:
1653 "Bulk %s is ripe: count %d >= %d" % (bulk_dir
, len(uuids
), count
))
1655 notify_log("Bulk %s is not ripe yet (age: %d, count: %d)!" % (bulk_dir
, age
,
1660 bulks
.append((bulk_dir
, age
, interval
, 'n.a.', count
, uuids
))
1663 active
= cmk_base
.core
.timeperiod_active(timeperiod
)
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
1669 notify_log("Error while checking activity of timeperiod %s: assuming active"
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
)))
1682 elif active
is False:
1684 "Bulk %s is ripe: timeperiod %s has ended" % (bulk_dir
, timeperiod
))
1685 elif len(uuids
) >= count
:
1687 "Bulk %s is ripe: count %d >= %d" % (bulk_dir
, len(uuids
), count
))
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
))
1697 def send_ripe_bulks():
1698 ripe
= find_bulks(True)
1700 notify_log("Sending out %d ripe bulk notifications" % len(ripe
))
1703 notify_bulk(bulk
[0], bulk
[-1])
1705 if cmk
.utils
.debug
.enabled():
1707 notify_log("Error sending bulk %s: %s" % (bulk
[0], format_exception()))
1710 def notify_bulk(dirname
, uuids
):
1711 parts
= dirname
.split("/")
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.
1721 unhandled_uuids
= []
1722 for mtime
, uuid
in uuids
:
1724 params
, context
= eval(file(dirname
+ "/" + uuid
).read())
1725 except Exception as e
:
1726 if cmk
.utils
.debug
.enabled():
1728 notify_log(" Deleting corrupted or empty bulk file %s/%s: %s" % (dirname
, uuid
, e
))
1731 if old_params
is None:
1733 elif params
!= old_params
:
1734 notify_log(" Parameters are different from previous, postponing into separate bulk")
1735 unhandled_uuids
.append((mtime
, uuid
))
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
)
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
)
1772 except Exception as e
:
1773 notify_log("Cannot remove %s: %s" % (path
, e
))
1775 # Repeat with unhandled uuids (due to different parameters)
1777 notify_bulk(dirname
, unhandled_uuids
)
1779 # Remove directory. Not neccessary if emtpy
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
)
1790 raise MKGeneralException("Notification plugin %s not found" % plugin
)
1792 stdout_txt
= stderr_txt
= ""
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
1799 p
= subprocess
.Popen([path
, "--bulk"],
1800 stdout
=subprocess
.PIPE
,
1801 stderr
=subprocess
.PIPE
,
1802 stdin
=subprocess
.PIPE
,
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
)
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------------------------------------------------------------.
1825 # | / ___|___ _ __ | |_ _____ _| |_ ___ |
1826 # | | | / _ \| '_ \| __/ _ \ \/ / __/ __| |
1827 # | | |__| (_) | | | | || __/> <| |_\__ \ |
1828 # | \____\___/|_| |_|\__\___/_/\_\\__|___/ |
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
):
1849 backlog
= eval(file(path
).read())[:config
.notification_backlog
- 1]
1853 backlog
= [raw_context
] + backlog
1854 file(path
, "w").write("%r\n" % backlog
)
1857 def raw_context_from_backlog(nr
):
1859 backlog
= eval(file(notification_logdir
+ "/backlog.mk").read())
1863 if nr
< 0 or nr
>= len(backlog
):
1864 console
.error("No notification number %d in backlog.\n" % nr
)
1867 notify_log("Replaying notification %d from backlog...\n" % 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
)
1890 # .--Helpers-------------------------------------------------------------.
1892 # | | | | | ___| |_ __ ___ _ __ ___ |
1893 # | | |_| |/ _ \ | '_ \ / _ \ '__/ __| |
1894 # | | _ | __/ | |_) | __/ | \__ \ |
1895 # | |_| |_|\___|_| .__/ \___|_| |___/ |
1897 # +----------------------------------------------------------------------+
1898 # | Some generic helper functions |
1899 # '----------------------------------------------------------------------'
1902 def format_exception():
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
):
1915 if value
[0] != '$' or value
[-1] != '$':
1917 for c
in value
[1:-1]:
1918 if not c
.isupper() and c
!= '_':
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
)
1940 return file('/proc/sys/kernel/random/uuid').read().strip()
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.
1946 return str(uuid
.uuid4())
1949 def _log_to_history(message
):
1950 _livestatus_cmd("LOG;%s" % message
)
1953 def _livestatus_cmd(command
):
1955 livestatus
.LocalConnection().command("[%d] %s" % (time
.time(), command
.encode("utf-8")))
1956 except Exception as e
:
1957 if cmk
.utils
.debug
.enabled():
1959 notify_log("WARNING: cannot send livestatus command: %s" % e
)
1960 notify_log("Command was: %s" % command
)