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
.regex
import regex
51 import cmk
.utils
.paths
52 from cmk
.utils
.exceptions
import MKGeneralException
55 import cmk_base
.config
as config
56 import cmk_base
.console
as console
58 import cmk_base
.events
as events
61 import cmk_base
.cee
.keepalive
as keepalive
63 keepalive
= None # type: ignore
65 _log_to_stdout
= False
66 notify_mode
= "notify"
68 # .--Configuration-------------------------------------------------------.
70 # | / ___|___ _ __ / _(_) __ _ _ _ _ __ __ _| |_(_) ___ _ __ |
71 # | | | / _ \| '_ \| |_| |/ _` | | | | '__/ _` | __| |/ _ \| '_ \ |
72 # | | |__| (_) | | | | _| | (_| | |_| | | | (_| | |_| | (_) | | | | |
73 # | \____\___/|_| |_|_| |_|\__, |\__,_|_| \__,_|\__|_|\___/|_| |_| |
75 # +----------------------------------------------------------------------+
76 # | Default values of global configuration variables. |
77 # '----------------------------------------------------------------------'
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$
97 Address: $HOSTADDRESS$
100 notification_host_body
= u
"""State: $LASTHOSTSTATE$ -> $HOSTSTATE$ ($NOTIFICATIONTYPE$)
101 Command: $HOSTCHECKCOMMAND$
103 Perfdata: $HOSTPERFDATA$
107 notification_service_body
= u
"""Service: $SERVICEDESC$
108 State: $LASTSERVICESTATE$ -> $SERVICESTATE$ ($NOTIFICATIONTYPE$)
109 Command: $SERVICECHECKCOMMAND$
110 Output: $SERVICEOUTPUT$
111 Perfdata: $SERVICEPERFDATA$
116 # .--helper--------------------------------------------------------------.
118 # | | |__ ___| |_ __ ___ _ __ |
119 # | | '_ \ / _ \ | '_ \ / _ \ '__| |
120 # | | | | | __/ | |_) | __/ | |
121 # | |_| |_|\___|_| .__/ \___|_| |
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----------------------------------------------------------------.
135 # | | \/ | __ _(_)_ __ |
136 # | | |\/| |/ _` | | '_ \ |
137 # | | | | | (_| | | | | | |
138 # | |_| |_|\__,_|_|_| |_| |
140 # +----------------------------------------------------------------------+
141 # | Main code entry point. |
142 # '----------------------------------------------------------------------'
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.
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
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
:
174 convert_legacy_configuration()
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'
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")
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
)
194 elif notify_mode
== 'spoolfile':
197 elif notify_mode
== 'replay':
199 replay_nr
= int(args
[1])
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():
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":
225 notify_notify(raw_context_from_env())
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]
241 config
.notification_spooling
= "both"
243 config
.notification_spooling
= "remote"
244 elif config
.notification_spooling
:
245 config
.notification_spooling
= "local"
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
257 def notify_notify(raw_context
, analyse
=False):
259 store_notification_backlog(raw_context
)
261 notify_log("----------------------------------------------------------------------")
263 notify_log("Analysing notification (%s) context with %s variables" %
264 (events
.find_host_service_in_context(raw_context
), len(raw_context
)))
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")
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
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
)
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." %
330 elif start
<= time
.time() <= end
:
332 "Notifications for %s are disabled in personal settings from %s to %s. Skipping."
333 % (contactname
, start
, end
))
336 # Get notification settings for the contact in question - if available.
338 method
= contact
.get("notification_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])
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():
355 notify_log("ERROR: %s\n%s" % (e
, format_exception()))
358 def notification_replay_backlog(nr
):
360 notify_mode
= "replay"
361 raw_context
= raw_context_from_backlog(nr
)
362 notify_notify(raw_context
)
365 def notification_analyse_backlog(nr
):
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)----------------------------------------.
375 # | | |/ /___ ___ _ __ __ _| (_)_ _____ |
376 # | | ' // _ \/ _ \ '_ \ / _` | | \ \ / / _ \ |
377 # | | . \ __/ __/ |_) | (_| | | |\ V / __/ |
378 # | |_|\_\___|\___| .__/ \__,_|_|_| \_/ \___| |
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--------------------------------------------.
400 # | | _ \ _ _| | ___| |__ __ _ ___ ___ __| | |
401 # | | |_) | | | | |/ _ \ '_ \ / _` / __|/ _ \/ _` | |
402 # | | _ <| |_| | | __/ |_) | (_| \__ \ __/ (_| | |
403 # | |_| \_\\__,_|_|\___|_.__/ \__,_|___/\___|\__,_| |
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.
417 # ( frozenset({"aa", "hh", "ll"}), "email" ) : ( False, [], None ),
418 # ( frozenset({"hh"}), "sms" ) : ( True, [ "0171737337", "bar", {
419 # 'groupby': 'host', 'interval': 60} ] ),
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"]))
430 notify_log("Global rule '%s'..." % rule
["description"])
432 why_not
= rbn_match_rule(rule
, raw_context
) # also checks disabling
434 notify_log(" -> does not match: %s" % why_not
)
435 rule_info
.append(("miss", rule
, why_not
))
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
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
:
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
))
473 notify_log(" - cancelling notification of %s via %s" % (", ".join(overlap
),
476 remaining
= notify_contacts
.difference(contacts
)
478 del notifications
[notify_key
]
480 new_key
= remaining
, plugin
481 notifications
[new_key
] = notifications
.pop(notify_key
)
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
))
490 " - modifying notification of %s via %s" % (contactstxt
, plugintxt
))
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
,
496 notifications
[key
] = (not rule
.get("allow_disable"), final_parameters
, bulk
)
498 rule_info
.append(("match", rule
, ""))
502 if not notifications
:
504 notify_log("%d rules matched, but no notification has been created." % num_rule_matches
)
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
)
516 notify_log("No rule matched, would notify fallback contacts, but none configured")
518 # Now do the actual notifications
519 notify_log("Executing %d notifications:" % len(notifications
))
520 entries
= notifications
.items()
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
,
532 plugin_context
= create_plugin_context(raw_context
, params
)
533 rbn_add_contact_information(plugin_context
, contacts
)
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
]
542 plugin_contexts
= rbn_split_plugin_context(plugin_context
)
544 for context
in plugin_contexts
:
545 plugin_info
.append((context
["CONTACTNAME"], plugin
, params
, bulk
))
550 do_bulk_notify(plugin
, params
, context
, bulk
)
551 elif config
.notification_spooling
in ("local", "both"):
552 create_spoolfile({"context": context
, "plugin": plugin
})
554 call_notification_script(plugin
, context
)
556 except Exception as e
:
557 if cmk
.utils
.debug
.enabled():
559 fe
= format_exception()
560 notify_log(" ERROR: %s" % e
)
563 analysis_info
= rule_info
, plugin_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"):
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
)
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
597 def user_notification_rules():
599 contactnames
= config
.contacts
.keys()
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
))
625 def rbn_fake_email_contact(email
):
627 "name": "mailto:" + email
,
628 "alias": "Explicit email adress " + email
,
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.
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
646 "alias": "Email address " + contact
,
647 "email": contact
[7:],
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("_")])
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
]
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
)
683 def rbn_get_bulk_params(rule
):
684 bulk
= rule
.get("bulk")
688 elif isinstance(bulk
, dict): # old format: treat as "Always Bulk"
689 method
, params
= "always", bulk
691 method
, params
= bulk
693 if method
== "always":
696 elif method
== "timeperiod":
698 active
= cmk_base
.core
.timeperiod_active(params
["timeperiod"])
700 if cmk
.utils
.debug
.enabled():
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"])
711 return params
.get("bulk_outside")
713 notify_log(" - Unknown bulking method: assuming bulking is disabled")
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))
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"):
745 from_number
, rate
= rule
["match_escalation_throttle"]
746 if context
["WHAT"] == "HOST":
747 notification_number
= int(context
.get("HOSTNOTIFICATIONNUMBER", 1))
749 notification_number
= int(context
.get("SERVICENOTIFICATIONNUMBER", 1))
750 if notification_number
<= from_number
:
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"]:
792 elif notification_type
in ["DOWNTIMESTART", "DOWNTIMEEND", "DOWNTIMECANCELLED"]:
794 elif notification_type
== "ACKNOWLEDGEMENT":
796 elif notification_type
.startswith("ALERTHANDLER ("):
797 handler_state
= notification_type
[14:-1]
798 if handler_state
== "OK":
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]):
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"]))
834 for contactname
in the_contacts
:
835 if contactname
== config
.notification_fallback_email
:
836 contact
= rbn_fake_email_contact(config
.notification_fallback_email
)
838 contact
= config
.contacts
.get(contactname
)
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:
846 " - skipping contact %s: he/she has disabled notifications" % contactname
)
848 elif start
<= time
.time() <= end
:
850 " - skipping contact %s: he/she has disabled notifications from %s to %s."
851 % (contactname
, start
, end
))
854 reason
= (rbn_match_contact_macros(rule
, contactname
, contact
) or
855 rbn_match_contact_groups(rule
, contactname
, contact
))
858 notify_log(" - skipping contact %s: %s" % (contactname
, reason
))
862 notify_log("Warning: cannot get information about contact %s: ignoring restrictions" %
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" %
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
[
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()]
955 return commasepped
.split(",")
960 def rbn_all_contacts(with_email
=None):
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
):
970 query
= "GET contactgroups\nColumns: members\n"
972 query
+= "Filter: name = %s\n" % group
973 query
+= "Or: %d\n" % len(groups
)
977 for contact_list
in livestatus
.LocalConnection().query_column(query
):
978 contacts
.update(contact_list
)
981 except livestatus
.MKLivestatusNotFoundError
:
985 if cmk
.utils
.debug
.enabled():
990 def rbn_emails_contacts(emails
):
991 return ["mailto:" + e
for e
in emails
]
995 # .--Flexible-Notifications----------------------------------------------.
997 # | | ___| | _____ _(_) |__ | | ___ |
998 # | | |_ | |/ _ \ \/ / | '_ \| |/ _ \ |
999 # | | _| | | __/> <| | |_) | | __/ |
1000 # | |_| |_|\___/_/\_\_|_.__/|_|\___| |
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
):
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
})
1022 call_notification_script(plugin
, plugin_context
)
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
):
1031 if entry
.get("disabled"):
1032 notify_log(" - Skipping: it is disabled for this user")
1035 # Check host, if configured
1036 if entry
.get("only_hosts"):
1037 hostname
= context
.get("HOSTNAME")
1042 for h
in entry
["only_hosts"]:
1043 if h
.startswith("!"): # negate
1046 elif h
.startswith('~'):
1050 if not regex_match
and hostname
== h
:
1054 elif regex_match
and re
.match(h
, hostname
):
1058 notify_log(" - Skipping: host '%s' matches none of %s" % (hostname
, ", ".join(
1059 entry
["only_hosts"])))
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'))
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
))
1074 # Skip blacklistet serivces
1075 if entry
.get("service_blacklist"):
1076 servicedesc
= context
.get("SERVICEDESC")
1078 notify_log(" - Proceed: blacklist certain services, but this is a host notification")
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"])))
1086 # Check service, if configured
1087 if entry
.get("only_services"):
1088 servicedesc
= context
.get("SERVICEDESC")
1090 notify_log(" - Proceed: limited to certain services, but this is a host notification")
1093 # only_services = [ "!LOG foo", "LOG", BAR" ]
1094 # -> notify all services beginning with LOG or BAR, but not "LOG foo..."
1096 for s
in entry
["only_services"]:
1097 if s
.startswith("!"): # negate
1102 if re
.match(s
, servicedesc
):
1106 notify_log(" - Skipping: service '%s' matches none of %s" % (servicedesc
, ", ".join(
1107 entry
["only_services"])))
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
)))
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))
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
))
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
)
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'}
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":
1152 elif notification_type
in ["FLAPPINGSTART", "FLAPPINGSTOP", "FLAPPINGDISABLED"]:
1154 elif notification_type
in ["DOWNTIMESTART", "DOWNTIMEEND", "DOWNTIMECANCELLED"]:
1156 elif notification_type
== "ACKNOWLEDGEMENT":
1159 event
= event_map
.get(state
, '?')
1161 return event
, allowed_events
1165 # .--Plain Email---------------------------------------------------------.
1166 # | ____ _ _ _____ _ _ |
1167 # | | _ \| | __ _(_)_ __ | ____|_ __ ___ __ _(_) | |
1168 # | | |_) | |/ _` | | '_ \ | _| | '_ ` _ \ / _` | | | |
1169 # | | __/| | (_| | | | | | | |___| | | | | | (_| | | | |
1170 # | |_| |_|\__,_|_|_| |_| |_____|_| |_| |_|\__,_|_|_| |
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})
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
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
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
)
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
1225 notify_log_debug("Executing command: %s" % command
)
1227 # TODO: Cleanup this shell=True call!
1228 p
= subprocess
.Popen( # nosec
1231 stdout
=subprocess
.PIPE
,
1232 stderr
=subprocess
.PIPE
,
1233 stdin
=subprocess
.PIPE
,
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
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())
1248 # .--Plugins-------------------------------------------------------------.
1250 # | | _ \| |_ _ __ _(_)_ __ ___ |
1251 # | | |_) | | | | |/ _` | | '_ \/ __| |
1252 # | | __/| | |_| | (_| | | | | \__ \ |
1253 # | |_| |_|\__,_|\__, |_|_| |_|___/ |
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
):
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
)
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
):
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
)
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'):
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'):
1320 'LONGSERVICEOUTPUT_HTML'] = u
'Check failed:\\nA crash report can be submitted via the user interface.'
1322 core_notification_log(plugin
, plugin_context
)
1325 notify_log(" %s" % s
)
1327 # The "Pseudo"-Plugin None means builtin plain email
1329 return notify_via_email(plugin_context
)
1331 # Call actual script without any arguments
1332 path
= path_to_notification_script(plugin
)
1336 plugin_log("executing %s" % path
)
1338 set_notification_timeout()
1339 p
= subprocess
.Popen([path
],
1340 stdout
=subprocess
.PIPE
,
1341 stderr
=subprocess
.STDOUT
,
1342 env
=notification_script_env(plugin_context
),
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()
1350 plugin_log("Output: %s" % line
.decode('utf-8').rstrip())
1352 console
.output(line
)
1355 # the stdout is closed but the return code may not be available just yet - wait for the
1356 # process to actually finish
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
)
1367 plugin_log("Plugin exited with code %d" % 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):
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():
1396 # .--Spooling------------------------------------------------------------.
1398 # | / ___| _ __ ___ ___ | (_)_ __ __ _ |
1399 # | \___ \| '_ \ / _ \ / _ \| | | '_ \ / _` | |
1400 # | ___) | |_) | (_) | (_) | | | | | | (_| | |
1401 # | |____/| .__/ \___/ \___/|_|_|_| |_|\__, | |
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("----------------------------------------------------------------------")
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()))
1454 # .--Bulk-Notifications--------------------------------------------------.
1456 # | | __ ) _ _| | | __ |
1457 # | | _ \| | | | | |/ / |
1458 # | | |_) | |_| | | < |
1459 # | |____/ \__,_|_|_|\_\ |
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:
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"]))
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", ""))
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
)
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("/")
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
1546 for char
in dirname
:
1547 if ord(char
) <= 0 or ord(char
) > 127:
1548 new_dirname
+= "%%%04x" % ord(char
)
1551 dirname
= new_dirname
1553 if not os
.path
.exists(dirname
):
1554 os
.makedirs(dirname
)
1555 notify_log(" - created bulk directory %s" % dirname
)
1559 def bulk_parts(method_dir
, bulk
):
1560 parts
= bulk
.split(',')
1563 interval
, timeperiod
= int(parts
[0]), None
1565 entry
= parts
[0].split(':')
1566 if entry
[0] == 'timeperiod' and len(entry
) == 2:
1567 interval
, timeperiod
= None, entry
[1]
1569 notify_log("Skipping invalid bulk directory %s" % method_dir
)
1573 count
= int(parts
[1])
1575 notify_log("Skipping invalid bulk directory %s" % method_dir
)
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"):
1587 notify_log("Skipping invalid notification file %s" % os
.path
.join(bulk_dir
, uuid
))
1590 mtime
= os
.stat(os
.path
.join(bulk_dir
, uuid
)).st_mtime
1591 uuids
.append((mtime
, uuid
))
1592 oldest
= min(oldest
, mtime
)
1594 return uuids
, oldest
1597 def remove_if_orphaned(bulk_dir
, max_age
, ref_time
=None):
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
)
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
):
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
)
1627 remove_if_orphaned(bulk_dir
, max_age
=60, ref_time
=now
)
1631 # e.g. 60,10,host,localhost OR timeperiod:late_night,1000,host,localhost
1632 parts
= bulk_parts(method_dir
, bulk
)
1635 interval
, timeperiod
, count
= parts
1637 if interval
is not None:
1639 notify_log("Bulk %s is ripe: age %d >= %d" % (bulk_dir
, age
, interval
))
1640 elif len(uuids
) >= count
:
1642 "Bulk %s is ripe: count %d >= %d" % (bulk_dir
, len(uuids
), count
))
1644 notify_log("Bulk %s is not ripe yet (age: %d, count: %d)!" % (bulk_dir
, age
,
1649 bulks
.append((bulk_dir
, age
, interval
, 'n.a.', count
, uuids
))
1652 active
= cmk_base
.core
.timeperiod_active(timeperiod
)
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
1658 notify_log("Error while checking activity of timeperiod %s: assuming active"
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
)))
1671 elif active
is False:
1673 "Bulk %s is ripe: timeperiod %s has ended" % (bulk_dir
, timeperiod
))
1674 elif len(uuids
) >= count
:
1676 "Bulk %s is ripe: count %d >= %d" % (bulk_dir
, len(uuids
), count
))
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
))
1686 def send_ripe_bulks():
1687 ripe
= find_bulks(True)
1689 notify_log("Sending out %d ripe bulk notifications" % len(ripe
))
1692 notify_bulk(bulk
[0], bulk
[-1])
1694 if cmk
.utils
.debug
.enabled():
1696 notify_log("Error sending bulk %s: %s" % (bulk
[0], format_exception()))
1699 def notify_bulk(dirname
, uuids
):
1700 parts
= dirname
.split("/")
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.
1710 unhandled_uuids
= []
1711 for mtime
, uuid
in uuids
:
1713 params
, context
= eval(file(dirname
+ "/" + uuid
).read())
1714 except Exception as e
:
1715 if cmk
.utils
.debug
.enabled():
1717 notify_log(" Deleting corrupted or empty bulk file %s/%s: %s" % (dirname
, uuid
, e
))
1720 if old_params
is None:
1722 elif params
!= old_params
:
1723 notify_log(" Parameters are different from previous, postponing into separate bulk")
1724 unhandled_uuids
.append((mtime
, uuid
))
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
)
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
)
1761 except Exception as e
:
1762 notify_log("Cannot remove %s: %s" % (path
, e
))
1764 # Repeat with unhandled uuids (due to different parameters)
1766 notify_bulk(dirname
, unhandled_uuids
)
1768 # Remove directory. Not neccessary if emtpy
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
)
1779 raise MKGeneralException("Notification plugin %s not found" % plugin
)
1781 stdout_txt
= stderr_txt
= ""
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
1788 p
= subprocess
.Popen([path
, "--bulk"],
1789 stdout
=subprocess
.PIPE
,
1790 stderr
=subprocess
.PIPE
,
1791 stdin
=subprocess
.PIPE
,
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
)
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------------------------------------------------------------.
1814 # | / ___|___ _ __ | |_ _____ _| |_ ___ |
1815 # | | | / _ \| '_ \| __/ _ \ \/ / __/ __| |
1816 # | | |__| (_) | | | | || __/> <| |_\__ \ |
1817 # | \____\___/|_| |_|\__\___/_/\_\\__|___/ |
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
):
1838 backlog
= eval(file(path
).read())[:config
.notification_backlog
- 1]
1842 backlog
= [raw_context
] + backlog
1843 file(path
, "w").write("%r\n" % backlog
)
1846 def raw_context_from_backlog(nr
):
1848 backlog
= eval(file(notification_logdir
+ "/backlog.mk").read())
1852 if nr
< 0 or nr
>= len(backlog
):
1853 console
.error("No notification number %d in backlog.\n" % nr
)
1856 notify_log("Replaying notification %d from backlog...\n" % 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
)
1879 # .--Helpers-------------------------------------------------------------.
1881 # | | | | | ___| |_ __ ___ _ __ ___ |
1882 # | | |_| |/ _ \ | '_ \ / _ \ '__/ __| |
1883 # | | _ | __/ | |_) | __/ | \__ \ |
1884 # | |_| |_|\___|_| .__/ \___|_| |___/ |
1886 # +----------------------------------------------------------------------+
1887 # | Some generic helper functions |
1888 # '----------------------------------------------------------------------'
1891 def format_exception():
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
):
1904 if value
[0] != '$' or value
[-1] != '$':
1906 for c
in value
[1:-1]:
1907 if not c
.isupper() and c
!= '_':
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:
1924 return file('/proc/sys/kernel/random/uuid').read().strip()
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.
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"]
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
):
1953 livestatus
.LocalConnection().command("[%d] %s" % (time
.time(), command
.encode("utf-8")))
1954 except Exception as e
:
1955 if cmk
.utils
.debug
.enabled():
1957 notify_log("WARNING: cannot send livestatus command: %s" % e
)
1958 notify_log("Command was: %s" % command
)