Fix typecheck idiom in cmk/gui/{plugins,wato}
[check_mk.git] / cmk / gui / wato / pages / notifications.py
blob37aef09c850c8c0a36372035075ab143c97be11b
1 #!/usr/bin/env python
2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
9 # | |
10 # | Copyright Mathias Kettner 2014 mk@mathias-kettner.de |
11 # +------------------------------------------------------------------+
13 # This file is part of Check_MK.
14 # The official homepage is at http://mathias-kettner.de/check_mk.
16 # check_mk is free software; you can redistribute it and/or modify it
17 # under the terms of the GNU General Public License as published by
18 # the Free Software Foundation in version 2. check_mk is distributed
19 # in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
20 # out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
21 # PARTICULAR PURPOSE. See the GNU General Public License for more de-
22 # tails. You should have received a copy of the GNU General Public
23 # License along with GNU Make; see the file COPYING. If not, write
24 # to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
25 # Boston, MA 02110-1301 USA.
26 """Modes for managing notification configuration"""
28 import abc
29 import time
31 import cmk
32 import cmk.store as store
34 import cmk.gui.view_utils
35 import cmk.gui.wato.user_profile
36 import cmk.gui.userdb as userdb
37 import cmk.gui.config as config
38 import cmk.gui.watolib as watolib
39 import cmk.gui.table as table
40 import cmk.gui.forms as forms
41 from cmk.gui.exceptions import MKUserError
42 from cmk.gui.i18n import _
43 from cmk.gui.globals import html
44 from cmk.gui.valuespec import (
45 Dictionary,
46 Alternative,
47 FixedValue,
48 Tuple,
49 Integer,
50 Transform,
51 ListOf,
52 EmailAddress,
53 ID,
54 DropdownChoice,
55 RegExp,
56 RegExpUnicode,
57 ListChoice,
58 Age,
59 CascadingDropdown,
60 TextAscii,
61 ListOfStrings,
62 Checkbox,
65 from cmk.gui.plugins.wato import (
66 EventsMode,
67 mode_registry,
68 wato_confirm,
69 global_buttons,
70 make_action_link,
71 add_change,
72 rule_option_elements,
76 class NotificationsMode(EventsMode):
77 # TODO: Clean this up. Use inheritance
78 @classmethod
79 def _rule_match_conditions(cls):
80 return cls._generic_rule_match_conditions() \
81 + cls._event_rule_match_conditions(flavour="notify") \
82 + cls._notification_rule_match_conditions()
84 @classmethod
85 def _notification_rule_match_conditions(cls):
86 def transform_ec_rule_id_match(val):
87 if isinstance(val, list):
88 return val
89 return [val]
91 return [
92 ( "match_escalation",
93 Tuple(
94 title = _("Restrict to n<sup>th</sup> to m<sup>th</sup> notification"),
95 orientation = "float",
96 elements = [
97 Integer(
98 label = _("from"),
99 help = _("Let through notifications counting from this number. "
100 "For normal alerts The first notification has the number 1. "
101 "For custom notifications the number is 0."),
102 default_value = 0,
103 minvalue = 0,
104 maxvalue = 999999,
106 Integer(
107 label = _("to"),
108 help = _("Let through notifications counting upto this number"),
109 default_value = 999999,
110 minvalue = 1,
111 maxvalue = 999999,
116 ( "match_escalation_throttle",
117 Tuple(
118 title = _("Throttle periodic notifications"),
119 help = _("This match option allows you to throttle periodic notifications after "
120 "a certain number of notifications have been created by the monitoring "
121 "core. If you for example select 10 as the beginning and 5 as the rate "
122 "then you will receive the notification 1 through 10 and then 15, 20, "
123 "25... and so on. Note that recovery notifications are not affected by throttling."),
124 orientation = "float",
125 elements = [
126 Integer(
127 label = _("beginning from notification number"),
128 default_value = 10,
129 minvalue = 1,
131 Integer(
132 label = _("send only every"),
133 default_value = 5,
134 unit = _("th notification"),
135 minvalue = 1,
140 ( "match_notification_comment",
141 RegExpUnicode(
142 title = _("Match notification comment"),
143 help = _("This match only makes sense for custom notifications. When a user creates "
144 "a custom notification then he/she can enter a comment. This comment is shipped "
145 "in the notification context variable <tt>NOTIFICATIONCOMMENT</tt>. Here you can "
146 "make a condition of that comment. It is a regular expression matching the beginning "
147 "of the comment."),
148 size = 60,
149 mode = RegExpUnicode.prefix,
151 ( "match_ec",
152 Alternative(
153 title = _("Event Console alerts"),
154 help = _("The Event Console can have events create notifications in Check_MK. "
155 "These notifications will be processed by the rule based notification "
156 "system of Check_MK. This matching option helps you distinguishing "
157 "and also gives you access to special event fields."),
158 style = "dropdown",
159 elements = [
160 FixedValue(False, title = _("Do not match Event Console alerts"), totext=""),
161 Dictionary(
162 title = _("Match only Event Console alerts"),
163 elements = [
164 ( "match_rule_id",
165 Transform(
166 ListOf(
167 ID(title = _("Match event rule"), label = _("Rule ID:"), size=12, allow_empty=False),
168 add_label = _("Add Rule ID"),
169 title = _("Rule IDs")
171 forth = transform_ec_rule_id_match,
174 ( "match_priority",
175 Tuple(
176 title = _("Match syslog priority"),
177 help = _("Define a range of syslog priorities this rule matches"),
178 orientation = "horizontal",
179 show_titles = False,
180 elements = [
181 DropdownChoice(label = _("from:"), choices = cmk.gui.mkeventd.syslog_priorities, default_value = 4),
182 DropdownChoice(label = _(" to:"), choices = cmk.gui.mkeventd.syslog_priorities, default_value = 0),
186 ( "match_facility",
187 DropdownChoice(
188 title = _("Match syslog facility"),
189 help = _("Make the rule match only if the event has a certain syslog facility. "
190 "Messages not having a facility are classified as <tt>user</tt>."),
191 choices = cmk.gui.mkeventd.syslog_facilities,
194 ( "match_comment",
195 RegExpUnicode(
196 title = _("Match event comment"),
197 help = _("This is a regular expression for matching the event's comment."),
198 mode = RegExpUnicode.prefix,
208 def _render_notification_rules(self,
209 rules,
210 userid="",
211 show_title=False,
212 show_buttons=True,
213 analyse=False,
214 start_nr=0,
215 profilemode=False):
216 if not rules:
217 html.message(_("You have not created any rules yet."))
218 return
220 vs_match_conditions = Dictionary(elements=self._rule_match_conditions())
222 if rules:
223 if not show_title:
224 title = ""
225 elif profilemode:
226 title = _("Notification rules")
227 elif userid:
228 url = html.makeuri([("mode", "user_notifications"), ("user", userid)])
229 code = html.render_icon_button(url, _("Edit this user's notifications"), "edit")
230 title = code + _("Notification rules of user %s") % userid
231 else:
232 title = _("Global notification rules")
233 table.begin(title=title, limit=None, sortable=False)
235 if analyse:
236 analyse_rules, _analyse_plugins = analyse
238 # have_match = False
239 for nr, rule in enumerate(rules):
240 table.row()
242 # Analyse
243 if analyse:
244 table.cell(css="buttons")
245 what, _anarule, reason = analyse_rules[nr + start_nr]
246 if what == "match":
247 html.icon(_("This rule matches"), "rulematch")
248 elif what == "miss":
249 html.icon(_("This rule does not match: %s") % reason, "rulenmatch")
251 if profilemode:
252 listmode = "user_notifications_p"
253 elif userid:
254 listmode = "user_notifications"
255 else:
256 listmode = "notifications"
258 actions_allowed = config.user.may(
259 "notification_plugin.%s" % rule['notify_plugin'][0])
261 if show_buttons and actions_allowed:
262 anavar = html.var("analyse", "")
263 delete_url = make_action_link([("mode", listmode), ("user", userid),
264 ("_delete", nr)])
265 drag_url = make_action_link([("mode", listmode), ("analyse", anavar),
266 ("user", userid), ("_move", nr)])
267 suffix = "_p" if profilemode else ""
268 edit_url = watolib.folder_preserving_link([("mode",
269 "notification_rule" + suffix),
270 ("edit", nr), ("user", userid)])
271 clone_url = watolib.folder_preserving_link([("mode",
272 "notification_rule" + suffix),
273 ("clone", nr), ("user", userid)])
275 table.cell(_("Actions"), css="buttons")
276 html.icon_button(edit_url, _("Edit this notification rule"), "edit")
277 html.icon_button(clone_url, _("Create a copy of this notification rule"), "clone")
278 html.element_dragger_url("tr", base_url=drag_url)
279 html.icon_button(delete_url, _("Delete this notification rule"), "delete")
280 else:
281 table.cell("", css="buttons")
282 for _x in xrange(4):
283 html.empty_icon_button()
285 table.cell("", css="narrow")
286 if rule.get("disabled"):
287 html.icon(_("This rule is currently disabled and will not be applied"), "disabled")
288 else:
289 html.empty_icon_button()
291 notify_method = rule["notify_plugin"]
292 # Catch rules with empty notify_plugin key
293 # Maybe this should be avoided somewhere else (e.g. rule editor)
294 if not notify_method:
295 notify_method = (None, [])
296 notify_plugin = notify_method[0]
298 table.cell(_("Type"), css="narrow")
299 if notify_method[1] == None:
300 html.icon(_("Cancel notifications for this plugin type"), "notify_cancel")
301 else:
302 html.icon(_("Create a notification"), "notify_create")
304 table.cell(_("Plugin"), notify_plugin or _("Plain Email"), css="narrow nowrap")
306 table.cell(_("Bulk"), css="narrow")
307 if "bulk" in rule or "bulk_period" in rule:
308 html.icon(_("This rule configures bulk notifications."), "bulk")
310 table.cell(_("Description"))
311 url = rule.get("docu_url")
312 if url:
313 html.icon_button(url, _("Context information about this rule"), "url", target="_blank")
314 html.write("&nbsp;")
315 html.write_text(rule["description"])
316 table.cell(_("Contacts"))
317 infos = []
318 if rule.get("contact_object"):
319 infos.append(_("all contacts of the notified object"))
320 if rule.get("contact_all"):
321 infos.append(_("all users"))
322 if rule.get("contact_all_with_email"):
323 infos.append(_("all users with and email address"))
324 if rule.get("contact_users"):
325 infos.append(_("users: ") + (", ".join(rule["contact_users"])))
326 if rule.get("contact_groups"):
327 infos.append(_("contact groups: ") + (", ".join(rule["contact_groups"])))
328 if rule.get("contact_emails"):
329 infos.append(_("email addresses: ") + (", ".join(rule["contact_emails"])))
330 if not infos:
331 html.i(_("(no one)"))
333 else:
334 for line in infos:
335 html.write("&bullet; %s" % line)
336 html.br()
338 table.cell(_("Conditions"), css="rule_conditions")
339 num_conditions = len([key for key in rule if key.startswith("match_")])
340 if num_conditions:
341 title = _("%d conditions") % num_conditions
342 html.begin_foldable_container(
343 treename="rule_%s_%d" % (userid, nr),
344 id_="%s" % nr,
345 isopen=False,
346 title=title,
347 indent=False,
348 tree_img="tree_black",
350 html.write(vs_match_conditions.value_to_text(rule))
351 html.end_foldable_container()
352 else:
353 html.i(_("(no conditions)"))
355 table.end()
357 def _add_change(self, log_what, log_text):
358 add_change(log_what, log_text, need_restart=False)
360 def _vs_notification_bulkby(self):
361 return ListChoice(
362 title = _("Create separate notification bulks based on"),
363 choices = [
364 ( "folder", _("Folder") ),
365 ( "host", _("Host") ),
366 ( "service", _("Service description") ),
367 ( "sl", _("Service level") ),
368 ( "check_type", _("Check type") ),
369 ( "state", _("Host/Service state") ),
370 ( "ec_contact", _("Event Console contact") ),
371 ( "ec_comment", _("Event Console comment") ),
373 default_value = [ "host" ],
377 @mode_registry.register
378 class ModeNotifications(NotificationsMode):
379 @classmethod
380 def name(cls):
381 return "notifications"
383 @classmethod
384 def permissions(cls):
385 return ["notifications"]
387 def __init__(self):
388 super(ModeNotifications, self).__init__()
389 options = config.user.load_file("notification_display_options", {})
390 self._show_user_rules = options.get("show_user_rules", False)
391 self._show_backlog = options.get("show_backlog", False)
392 self._show_bulks = options.get("show_bulks", False)
394 def title(self):
395 return _("Notification configuration")
397 def buttons(self):
398 global_buttons()
399 html.context_button(_("New Rule"), watolib.folder_preserving_link([("mode", "notification_rule")]), "new")
400 if self._show_user_rules:
401 html.context_button(_("Hide user rules"), html.makeactionuri([("_show_user", "")]), "users")
402 else:
403 html.context_button(_("Show user rules"), html.makeactionuri([("_show_user", "1")]), "users")
405 if self._show_backlog:
406 html.context_button(_("Hide Analysis"), html.makeactionuri([("_show_backlog", "")]), "analyze")
407 else:
408 html.context_button(_("Analyse"), html.makeactionuri([("_show_backlog", "1")]), "analyze")
410 if self._show_bulks:
411 html.context_button(_("Hide Bulks"), html.makeactionuri([("_show_bulks", "")]), "bulk")
412 else:
413 html.context_button(_("Show Bulks"), html.makeactionuri([("_show_bulks", "1")]), "bulk")
415 def action(self):
416 if html.has_var("_show_user"):
417 if html.check_transaction():
418 self._show_user_rules = bool(html.var("_show_user"))
419 self._save_notification_display_options()
421 elif html.has_var("_show_backlog"):
422 if html.check_transaction():
423 self._show_backlog = bool(html.var("_show_backlog"))
424 self._save_notification_display_options()
426 elif html.has_var("_show_bulks"):
427 if html.check_transaction():
428 self._show_bulks = bool(html.var("_show_bulks"))
429 self._save_notification_display_options()
431 elif html.has_var("_replay"):
432 if html.check_transaction():
433 nr = int(html.var("_replay"))
434 watolib.check_mk_local_automation("notification-replay", [str(nr)], None)
435 return None, _("Replayed notifiation number %d") % (nr + 1)
437 else:
438 return self._generic_rule_list_actions(self._get_notification_rules(), "notification", _("notification rule"), watolib.save_notification_rules)
440 def _get_notification_rules(self):
441 return watolib.load_notification_rules()
443 def _save_notification_display_options(self):
444 config.user.save_file(
445 "notification_display_options", {
446 "show_user_rules": self._show_user_rules,
447 "show_backlog": self._show_backlog,
448 "show_bulks": self._show_bulks,
451 def page(self):
452 self._show_not_enabled_warning()
453 self._show_no_fallback_contact_warning()
454 self._show_bulk_notifications()
455 self._show_notification_backlog()
456 self._show_rules()
458 def _show_not_enabled_warning(self):
459 # Check setting of global notifications. Are they enabled? If not, display
460 # a warning here. Note: this is a main.mk setting, so we cannot access this
461 # directly.
462 current_settings = watolib.load_configuration_settings()
463 if not current_settings.get("enable_rulebased_notifications"):
464 url = 'wato.py?mode=edit_configvar&varname=enable_rulebased_notifications'
465 html.show_warning(
466 _("<b>Warning</b><br><br>Rule based notifications are disabled in your global settings. "
467 "The rules that you edit here will have affect only on notifications that are "
468 "created by the Event Console. Normal monitoring alerts will <b>not</b> use the "
469 "rule based notifications now."
470 "<br><br>"
471 "You can change this setting <a href=\"%s\">here</a>.") % url)
473 def _show_no_fallback_contact_warning(self):
474 if not self._fallback_mail_contacts_configured():
475 url = 'wato.py?mode=edit_configvar&varname=notification_fallback_email'
476 html.show_warning(
477 _("<b>Warning</b><br><br>You haven't configured a "
478 "<a href=\"%s\">fallback email address</a> nor enabled receiving fallback emails for "
479 "any user. If your monitoring produces a notification that is not matched by any of your "
480 "notification rules, the notification will not be sent out. To prevent that, please "
481 "configure either the global setting or enable the fallback contact option for at least "
482 "one of your users.") % url)
484 def _fallback_mail_contacts_configured(self):
485 current_settings = watolib.load_configuration_settings()
486 if current_settings.get("notification_fallback_email"):
487 return True
489 for user in userdb.load_users(lock=False).itervalues():
490 if user.get("fallback_contact", False):
491 return True
493 return False
495 def _show_bulk_notifications(self):
496 if self._show_bulks:
497 # Warn if there are unsent bulk notifications
498 if not self._render_bulks(only_ripe=False):
499 html.message(_("Currently there are no unsent notification bulks pending."))
500 else:
501 # Warn if there are unsent bulk notifications
502 self._render_bulks(only_ripe=True)
504 def _render_bulks(self, only_ripe):
505 bulks = watolib.check_mk_local_automation("notification-get-bulks",
506 ["1" if only_ripe else "0"], None)
507 if not bulks:
508 return False
510 if only_ripe:
511 table.begin(title = _("Overdue bulk notifications!"))
512 else:
513 table.begin(title = _("Open bulk notifications"))
515 for directory, age, interval, timeperiod, maxcount, uuids in bulks:
516 dirparts = directory.split("/")
517 contact = dirparts[-3]
518 method = dirparts[-2]
519 bulk_id = dirparts[-1].split(",", 2)[-1]
520 table.row()
521 table.cell(_("Contact"), contact)
522 table.cell(_("Method"), method)
523 table.cell(_("Bulk ID"), bulk_id)
524 table.cell(_("Max. Age (sec)"), "%s" % interval, css="number")
525 table.cell(_("Age (sec)"), "%d" % age, css="number")
526 if interval and age >= interval:
527 html.icon(_("Age of oldest notification is over maximum age"), "warning")
528 table.cell(_("Timeperiod"), "%s" % timeperiod)
529 table.cell(_("Max. Count"), str(maxcount), css="number")
530 table.cell(_("Count"), str(len(uuids)), css="number")
531 if len(uuids) >= maxcount:
532 html.icon(_("Number of notifications exceeds maximum allowed number"), "warning")
533 table.end()
534 return True
536 def _show_notification_backlog(self):
537 """Show recent notifications. We can use them for rule analysis"""
538 if not self._show_backlog:
539 return
541 backlog = store.load_data_from_file(cmk.paths.var_dir + "/notify/backlog.mk", [])
542 if not backlog:
543 return
545 table.begin(table_id = "backlog", title = _("Recent notifications (for analysis)"), sortable=False)
546 for nr, context in enumerate(backlog):
547 self._convert_context_to_unicode(context)
548 table.row()
549 table.cell("&nbsp;", css="buttons")
551 analyse_url = html.makeuri([("analyse", str(nr))])
552 html.icon_button(analyse_url, _("Analyze ruleset with this notification"), "analyze")
554 html.icon_button(None, _("Show / hide notification context"),
555 "toggle_context",
556 onclick="toggle_container('notification_context_%d')" % nr)
558 replay_url = html.makeactionuri([("_replay", str(nr))])
559 html.icon_button(replay_url, _("Replay this notification, send it again!"), "replay")
561 if html.var("analyse") and nr == int(html.var("analyse")):
562 html.icon(_("You are analysing this notification"), "rulematch")
564 table.cell(_("Nr."), nr+1, css="number")
565 if "MICROTIME" in context:
566 date = time.strftime("%Y-%m-%d %H:%M:%S",
567 time.localtime(int(context["MICROTIME"]) / 1000000.0))
568 else:
569 date = context.get("SHORTDATETIME") or \
570 context.get("LONGDATETIME") or \
571 context.get("DATE") or \
572 _("Unknown date")
574 table.cell(_("Date/Time"), date, css="nobr")
575 nottype = context.get("NOTIFICATIONTYPE", "")
576 table.cell(_("Type"), nottype)
578 if nottype in ["PROBLEM", "RECOVERY"]:
579 if context.get("SERVICESTATE"):
580 statename = context["SERVICESTATE"][:4]
581 state = context["SERVICESTATEID"]
582 css = "state svcstate state%s" % state
583 else:
584 statename = context.get("HOSTSTATE")[:4]
585 state = context["HOSTSTATEID"]
586 css = "state hstate hstate%s" % state
587 table.cell(_("State"), statename, css=css)
588 elif nottype.startswith("DOWNTIME"):
589 table.cell(_("State"))
590 html.icon(_("Downtime"), "downtime")
591 elif nottype.startswith("ACK"):
592 table.cell(_("State"))
593 html.icon(_("Acknowledgement"), "ack")
594 elif nottype.startswith("FLAP"):
595 table.cell(_("State"))
596 html.icon(_("Flapping"), "flapping")
597 else:
598 table.cell(_("State"), "")
600 table.cell(_("Host"), context.get("HOSTNAME", ""))
601 table.cell(_("Service"), context.get("SERVICEDESC", ""))
602 output = context.get("SERVICEOUTPUT", context.get("HOSTOUTPUT"))
604 table.cell(_("Plugin output"), cmk.gui.view_utils.format_plugin_output(output, shall_escape=config.escape_plugin_output))
606 # Add toggleable notitication context
607 table.row(class_="notification_context hidden", id_="notification_context_%d" % nr)
608 table.cell(colspan=8)
610 html.open_table()
611 for nr, (key, val) in enumerate(sorted(context.items())):
612 if nr % 2 == 0:
613 if nr != 0:
614 html.close_tr()
615 html.open_tr()
616 html.th(key)
617 html.td(val)
618 html.close_table()
620 # This dummy row is needed for not destroying the odd/even row highlighting
621 table.row(class_="notification_context hidden")
623 table.end()
625 def _convert_context_to_unicode(self, context):
626 # Convert all values to unicode
627 for key, value in context.iteritems():
628 if isinstance(value, str):
629 try:
630 value_unicode = value.decode("utf-8")
631 except:
632 try:
633 value_unicode = value.decode("latin-1")
634 except:
635 value_unicode = u"(Invalid byte sequence)"
636 context[key] = value_unicode
638 # TODO: Refactor this
639 def _show_rules(self):
640 # Do analysis
641 if html.var("analyse"):
642 nr = int(html.var("analyse"))
643 analyse = watolib.check_mk_local_automation("notification-analyse", [str(nr)], None)
644 else:
645 analyse = False
647 start_nr = 0
648 rules = self._get_notification_rules()
649 self._render_notification_rules(rules, show_title=True, analyse=analyse, start_nr=start_nr)
650 start_nr += len(rules)
652 if self._show_user_rules:
653 users = userdb.load_users()
654 userids = users.keys()
655 userids.sort() # Create same order as modules/notification.py
656 for userid in userids:
657 user = users[userid]
658 user_rules = user.get("notification_rules", [])
659 if user_rules:
660 self._render_notification_rules(
661 user_rules,
662 userid,
663 show_title=True,
664 show_buttons=False,
665 analyse=analyse,
666 start_nr=start_nr)
667 start_nr += len(user_rules)
669 if analyse:
670 table.begin(table_id = "plugins", title = _("Resulting notifications"))
671 for contact, plugin, parameters, bulk in analyse[1]:
672 table.row()
673 if contact.startswith('mailto:'):
674 contact = contact[7:] # strip of fake-contact mailto:-prefix
675 table.cell(_("Recipient"), contact)
676 table.cell(_("Plugin"), self._vs_notification_scripts().value_to_text(plugin))
677 table.cell(_("Plugin parameters"), ", ".join(parameters))
678 table.cell(_("Bulking"))
679 if bulk:
680 html.write(_("Time horizon") + ": " + Age().value_to_text(bulk["interval"]))
681 html.write_text(", %s: %d" % (_("Maximum count"), bulk["count"]))
682 html.write(", %s %s" % (_("group by"), self._vs_notification_bulkby().value_to_text(bulk["groupby"])))
684 table.end()
686 def _vs_notification_scripts(self):
687 return DropdownChoice(
688 title = _("Notification Script"),
689 choices = watolib.notification_script_choices,
690 default_value = "mail"
694 class UserNotificationsMode(NotificationsMode):
695 def __init__(self):
696 super(UserNotificationsMode, self).__init__()
697 self._start_async_repl = False
699 def _from_vars(self):
700 self._users = userdb.load_users(lock=html.is_transaction() or html.has_var("_move"))
702 try:
703 user = self._users[self._user_id()]
704 except KeyError:
705 raise MKUserError(None, _('The requested user does not exist'))
707 self._rules = user.setdefault("notification_rules", [])
709 @abc.abstractmethod
710 def _user_id(self):
711 raise NotImplementedError()
713 def title(self):
714 return _("Custom notification table for user ") + self._user_id()
716 def buttons(self):
717 html.context_button(_("All Users"), watolib.folder_preserving_link([("mode", "users")]), "back")
718 html.context_button(_("User Properties"),
719 watolib.folder_preserving_link([("mode", "edit_user"), ("edit", self._user_id())]), "edit")
720 html.context_button(_("New Rule"),
721 watolib.folder_preserving_link([("mode", "notification_rule"), ("user", self._user_id())]), "new")
723 def action(self):
724 if html.has_var("_delete"):
725 nr = int(html.var("_delete"))
726 rule = self._rules[nr]
727 c = wato_confirm(_("Confirm notification rule deletion"),
728 _("Do you really want to delete the notification rule <b>%d</b> <i>%s</i>?") %
729 (nr, rule.get("description","")))
730 if c:
731 del self._rules[nr]
732 userdb.save_users(self._users)
734 self._add_change("notification-delete-user-rule",
735 _("Deleted notification rule %d of user %s") % (nr, self._user_id()))
736 elif c == False:
737 return ""
738 else:
739 return
741 elif html.has_var("_move"):
742 if html.check_transaction():
743 from_pos = html.get_integer_input("_move")
744 to_pos = html.get_integer_input("_index")
745 rule = self._rules[from_pos]
746 del self._rules[from_pos] # make to_pos now match!
747 self._rules[to_pos:to_pos] = [rule]
748 userdb.save_users(self._users)
750 self._add_change("notification-move-user-rule",
751 _("Changed position of notification rule %d of user %s") % (from_pos, self._user_id()))
753 def page(self):
754 if self._start_async_repl:
755 cmk.gui.wato.user_profile.user_profile_async_replication_dialog(
756 sites=watolib.get_notification_sync_sites())
757 html.h3(_('Notification Rules'))
759 self._render_notification_rules(
760 self._rules,
761 self._user_id(),
762 profilemode=isinstance(self, ModePersonalUserNotifications))
765 @mode_registry.register
766 class ModeUserNotifications(UserNotificationsMode):
767 @classmethod
768 def name(cls):
769 return "user_notifications"
771 @classmethod
772 def permissions(cls):
773 return ["users"]
775 def _user_id(self):
776 return html.get_unicode_input("user")
779 @mode_registry.register
780 class ModePersonalUserNotifications(UserNotificationsMode):
781 @classmethod
782 def name(cls):
783 return "user_notifications_p"
785 @classmethod
786 def permissions(cls):
787 return None
789 def __init__(self):
790 super(ModePersonalUserNotifications, self).__init__()
791 config.user.need_permission("general.edit_notifications")
793 def _user_id(self):
794 return config.user.id
796 def _add_change(self, log_what, log_text):
797 if config.has_wato_slave_sites():
798 self._start_async_repl = True
799 watolib.log_audit(None, log_what, log_text)
800 else:
801 super(ModePersonalUserNotifications, self)._add_change(log_what, log_text)
803 def title(self):
804 return _("Your personal notification rules")
806 def buttons(self):
807 html.context_button(_("Profile"), "user_profile.py", "back")
808 html.context_button(_("New Rule"), watolib.folder_preserving_link([("mode", "notification_rule_p")]), "new")
811 # TODO: Split editing of user notification rule and global notification rule
812 # into separate classes
813 class EditNotificationRuleMode(NotificationsMode):
814 def __init__(self):
815 super(EditNotificationRuleMode, self).__init__()
816 self._start_async_repl = False
818 # TODO: Refactor this
819 def _from_vars(self):
820 self._edit_nr = html.get_integer_input("edit", -1)
821 self._clone_nr = html.get_integer_input("clone", -1)
822 self._new = self._edit_nr < 0
824 if self._user_id():
825 self._users = userdb.load_users(lock=html.is_transaction())
826 if self._user_id() not in self._users:
827 raise MKUserError(None, _("The user you are trying to edit "
828 "notification rules for does not exist."))
829 user = self._users[self._user_id()]
830 self._rules = user.setdefault("notification_rules", [])
831 else:
832 self._rules = watolib.load_notification_rules(lock=html.is_transaction())
834 if self._new:
835 if self._clone_nr >= 0 and not html.var("_clear"):
836 self._rule = {}
837 try:
838 self._rule.update(self._rules[self._clone_nr])
839 except IndexError:
840 raise MKUserError(None, _("This %s does not exist.") % "notification rule")
841 else:
842 self._rule = {}
843 else:
844 try:
845 self._rule = self._rules[self._edit_nr]
846 except IndexError:
847 raise MKUserError(None, _("This %s does not exist.") % "notification rule")
849 def _valuespec(self):
850 return self._vs_notification_rule(self._user_id())
852 # TODO: Refactor this mess
853 def _vs_notification_rule(self, userid=None):
854 if userid:
855 contact_headers = []
856 section_contacts = []
857 section_override = []
858 else:
859 contact_headers = [
860 ( _("Contact Selection"), [ "contact_all", "contact_all_with_email", "contact_object",
861 "contact_users", "contact_groups", "contact_emails", "contact_match_macros",
862 "contact_match_groups", ] ),
864 section_contacts = [
865 # Contact selection
866 ( "contact_object",
867 Checkbox(
868 title = _("All contacts of the notified object"),
869 label = _("Notify all contacts of the notified host or service."),
870 default_value = True,
873 ( "contact_all",
874 Checkbox(
875 title = _("All users"),
876 label = _("Notify all users"),
879 ( "contact_all_with_email",
880 Checkbox(
881 title = _("All users with an email address"),
882 label = _("Notify all users that have configured an email address in their profile"),
885 ( "contact_users",
886 ListOf(
887 watolib.UserSelection(only_contacts = False),
888 title = _("The following users"),
889 help = _("Enter a list of user IDs to be notified here. These users need to be members "
890 "of at least one contact group in order to be notified."),
891 movable = False,
892 add_label = _("Add user"),
895 ( "contact_groups",
896 ListOf(
897 cmk.gui.plugins.wato.GroupSelection("contact"),
898 title = _("The members of certain contact groups"),
899 movable = False,
902 ( "contact_emails",
903 ListOfStrings(
904 valuespec = EmailAddress(size = 44),
905 title = _("The following explicit email addresses"),
906 orientation = "vertical",
909 ( "contact_match_macros",
910 ListOf(
911 Tuple(
912 elements = [
913 TextAscii(
914 title = _("Name of the macro"),
915 help = _("As configured in the users settings. Do not add a leading underscore."),
916 allow_empty = False,
918 RegExp(
919 title = _("Required match (regular expression)"),
920 help = _("This expression must match the value of the variable"),
921 allow_empty = False,
922 mode = RegExp.complete,
926 title = _("Restrict by custom macros"),
927 help = _("Here you can <i>restrict</i> the list of contacts that has been "
928 "built up by the previous options to those who have certain values "
929 "in certain custom macros. If you add more than one macro here then "
930 "<i>all</i> macros must match. The matches are regular expressions "
931 "that must fully match the value of the macro."),
932 add_label = _("Add condition"),
935 ( "contact_match_groups",
936 ListOf(
937 cmk.gui.plugins.wato.GroupSelection("contact"),
938 title = _("Restrict by contact groups"),
939 help = _("Here you can <i>restrict</i> the list of contacts that has been "
940 "built up by the previous options to those that are members of "
941 "selected contact groups. If you select more than one contact group here then "
942 "the user must be member of <i>all</i> these groups."),
943 add_label = _("Add Group"),
944 movable = False,
948 section_override = [
949 ( "allow_disable",
950 Checkbox(
951 title = _("Overriding by users"),
952 help = _("If you uncheck this option then users are not allowed to deactive notifications "
953 "that are created by this rule."),
954 label = _("allow users to deactivate this notification"),
955 default_value = True,
960 bulk_options = [
961 ("count", Integer(
962 title = _("Maximum bulk size"),
963 label = _("Bulk up to"),
964 unit = _("Notifications"),
965 help = _("At most that many Notifications are kept back for bulking. A value of "
966 "1 essentially turns off notification bulking."),
967 default_value = 1000,
968 minvalue = 1,
970 ("groupby",
971 self._vs_notification_bulkby(),
973 ("groupby_custom", ListOfStrings(
974 valuespec = ID(),
975 orientation = "horizontal",
976 title = _("Create separate notification bulks for different values of the following custom macros"),
977 help = _("If you enter the names of host/service-custom macros here then for each different "
978 "combination of values of those macros a separate bulk will be created. This can be used "
979 "in combination with the grouping by folder, host etc. Omit any leading underscore. "
980 "<b>Note</b>: If you are using "
981 "Nagios as a core you need to make sure that the values of the required macros are "
982 "present in the notification context. This is done in <tt>check_mk_templates.cfg</tt>. If you "
983 "macro is <tt>_FOO</tt> then you need to add the variables <tt>NOTIFY_HOST_FOO</tt> and "
984 "<tt>NOTIFY_SERVICE_FOO</tt>."),
986 ("bulk_subject", TextAscii(
987 title = _("Subject for bulk notifications"),
988 help = _("Customize the subject for bulk notifications and overwrite "
989 "default subject <tt>Check_MK: $COUNT_NOTIFICATIONS$ notifications for HOST</tt>"
990 " resp. <tt>Check_MK: $COUNT_NOTIFICATIONS$ notifications for $COUNT_HOSTS$ hosts</tt>. "
991 "Both macros <tt>$COUNT_NOTIFICATIONS$</tt> and <tt>$COUNT_HOSTS$</tt> can be used in "
992 "any customized subject. If <tt>$COUNT_NOTIFICATIONS$</tt> is used, the amount of "
993 "notifications will be inserted and if you use <tt>$COUNT_HOSTS$</tt> then the "
994 "amount of hosts will be applied."),
995 size = 80,
996 default_value = "Check_MK: $COUNT_NOTIFICATIONS$ notifications for $COUNT_HOSTS$ hosts"
1000 return Dictionary(
1001 title = _("Rule Properties"),
1002 elements = rule_option_elements()
1003 + section_override
1004 + self._rule_match_conditions()
1005 + section_contacts
1007 # Notification
1008 ( "notify_plugin",
1009 watolib.get_vs_notification_methods(),
1011 ("bulk", Transform(
1012 CascadingDropdown(
1013 title = "Notification Bulking",
1014 orientation = "vertical",
1015 choices = [
1016 ("always", _("Always bulk"), Dictionary(
1017 help = _("Enabling the bulk notifications will collect several subsequent notifications "
1018 "for the same contact into one single notification, which lists of all the "
1019 "actual problems, e.g. in a single email. This cuts down the number of notifications "
1020 "in cases where many (related) problems occur within a short time."),
1021 elements = [
1022 ( "interval", Age(
1023 title = _("Time horizon"),
1024 label = _("Bulk up to"),
1025 help = _("Notifications are kept back for bulking at most for this time."),
1026 default_value = 60,
1028 ] + bulk_options,
1029 columns = 1,
1030 optional_keys = ["bulk_subject"],
1032 ("timeperiod", _("Bulk during timeperiod"), Dictionary(
1033 help = _("By enabling this option notifications will be bulked only if the "
1034 "specified timeperiod is active. When the timeperiod ends a "
1035 "bulk containing all notifications that appeared during that time "
1036 "will be sent. "
1037 "If bulking should be enabled outside of the timeperiod as well, "
1038 "the option \"Also Bulk outside of timeperiod\" can be used."),
1039 elements = [
1040 ("timeperiod",
1041 watolib.TimeperiodSelection(
1042 title = _("Only bulk notifications during the following timeperiod"),
1044 ] + bulk_options + [
1045 ("bulk_outside", Dictionary(
1046 title = _("Also bulk outside of timeperiod"),
1047 help = _("By enabling this option notifications will be bulked "
1048 "outside of the defined timeperiod as well."),
1049 elements = [
1050 ( "interval", Age(
1051 title = _("Time horizon"),
1052 label = _("Bulk up to"),
1053 help = _("Notifications are kept back for bulking at most for this time."),
1054 default_value = 60,
1056 ] + bulk_options,
1057 columns = 1,
1058 optional_keys = ["bulk_subject"],
1061 columns = 1,
1062 optional_keys = ["bulk_subject", "bulk_outside"],
1066 forth = lambda x: x if isinstance(x, tuple) else ("always", x)
1069 optional_keys = [ "match_site", "match_folder", "match_hosttags", "match_hostgroups", "match_hosts", "match_exclude_hosts",
1070 "match_servicegroups", "match_exclude_servicegroups", "match_servicegroups_regex", "match_exclude_servicegroups_regex",
1071 "match_services", "match_exclude_services",
1072 "match_contacts", "match_contactgroups",
1073 "match_plugin_output",
1074 "match_timeperiod", "match_escalation", "match_escalation_throttle",
1075 "match_sl", "match_host_event", "match_service_event", "match_ec", "match_notification_comment",
1076 "match_checktype", "bulk", "contact_users", "contact_groups", "contact_emails",
1077 "contact_match_macros", "contact_match_groups" ],
1078 headers = [
1079 ( _("Rule Properties"), [ "description", "comment", "disabled", "docu_url", "allow_disable" ] ),
1080 ( _("Notification Method"), [ "notify_plugin", "notify_method", "bulk" ] ),]
1081 + contact_headers
1083 ( _("Conditions"), [ "match_site", "match_folder", "match_hosttags", "match_hostgroups",
1084 "match_hosts", "match_exclude_hosts", "match_servicegroups",
1085 "match_exclude_servicegroups", "match_servicegroups_regex", "match_exclude_servicegroups_regex",
1086 "match_services", "match_exclude_services",
1087 "match_checktype",
1088 "match_contacts", "match_contactgroups",
1089 "match_plugin_output",
1090 "match_timeperiod",
1091 "match_escalation", "match_escalation_throttle",
1092 "match_sl", "match_host_event", "match_service_event", "match_ec", "match_notification_comment" ] ),
1094 render = "form",
1095 form_narrow = True,
1096 validate = self._validate_notification_rule,
1099 def _validate_notification_rule(self, rule, varprefix):
1100 if "bulk" in rule and rule["notify_plugin"][1] == None:
1101 raise MKUserError(varprefix + "_p_bulk_USE",
1102 _("It does not make sense to add a bulk configuration for cancelling rules."))
1104 if "bulk" in rule or "bulk_period" in rule:
1105 if rule["notify_plugin"][0]:
1106 info = watolib.load_notification_scripts()[rule["notify_plugin"][0]]
1107 if not info["bulk"]:
1108 raise MKUserError(varprefix + "_p_notify_plugin",
1109 _("The notification script %s does not allow bulking.") % info["title"])
1110 else:
1111 raise MKUserError(varprefix + "_p_notify_plugin",
1112 _("Legacy ASCII Emails do not support bulking. You can either disable notification "
1113 "bulking or choose another notification plugin which allows bulking."))
1115 @abc.abstractmethod
1116 def _user_id(self):
1117 raise NotImplementedError()
1119 @abc.abstractmethod
1120 def _back_mode(self):
1121 raise NotImplementedError()
1123 def title(self):
1124 if self._new:
1125 if self._user_id():
1126 return _("Create new notification rule for user %s") % self._user_id()
1127 return _("Create new notification rule")
1129 if self._user_id():
1130 return _("Edit notification rule %d of user %s") % (self._edit_nr, self._user_id())
1131 return _("Edit notification rule %d") % self._edit_nr
1133 def buttons(self):
1134 html.context_button(_("All Rules"),
1135 watolib.folder_preserving_link([("mode", "notifications"), ("userid", self._user_id())]), "back")
1137 def action(self):
1138 if not html.check_transaction():
1139 return self._back_mode()
1141 vs = self._valuespec()
1142 self._rule = vs.from_html_vars("rule")
1143 if self._user_id():
1144 self._rule["contact_users"] = [self._user_id()] # Force selection of our user
1146 vs.validate_value(self._rule, "rule")
1148 if self._user_id():
1149 # User rules are always allow_disable
1150 # The parameter is set just after the validation, since the allow_disable
1151 # key isn't in the valuespec. Curiously, the validation does not fail
1152 # even the allow_disable key is set before the validate_value...
1153 self._rule["allow_disable"] = True
1155 if self._new and self._clone_nr >= 0:
1156 self._rules[self._clone_nr:self._clone_nr] = [self._rule]
1157 elif self._new:
1158 self._rules[0:0] = [self._rule]
1159 else:
1160 self._rules[self._edit_nr] = self._rule
1162 if self._user_id():
1163 userdb.save_users(self._users)
1164 else:
1165 watolib.save_notification_rules(self._rules)
1167 if self._new:
1168 log_what = "new-notification-rule"
1169 if self._user_id():
1170 log_text = _("Created new notification rule for user %s") % self._user_id()
1171 else:
1172 log_text = _("Created new notification rule")
1173 else:
1174 log_what = "edit-notification-rule"
1175 if self._user_id():
1176 log_text = _("Changed notification rule %d of user %s") % (self._edit_nr, self._user_id())
1177 else:
1178 log_text = _("Changed notification rule %d") % self._edit_nr
1179 self._add_change(log_what, log_text)
1181 return self._back_mode()
1183 def page(self):
1184 if self._start_async_repl:
1185 cmk.gui.wato.user_profile.user_profile_async_replication_dialog(
1186 sites=watolib.get_notification_sync_sites())
1187 return
1189 html.begin_form("rule", method="POST")
1190 vs = self._valuespec()
1191 vs.render_input("rule", self._rule)
1192 vs.set_focus("rule")
1193 forms.end()
1194 html.button("save", _("Save"))
1195 html.hidden_fields()
1196 html.end_form()
1199 @mode_registry.register
1200 class ModeEditNotificationRule(EditNotificationRuleMode):
1201 @classmethod
1202 def name(cls):
1203 return "notification_rule"
1205 @classmethod
1206 def permissions(cls):
1207 return ["notifications"]
1209 def _user_id(self):
1210 return html.get_unicode_input("user")
1212 def _back_mode(self):
1213 if self._user_id():
1214 return "user_notifications"
1215 return "notifications"
1218 @mode_registry.register
1219 class ModeEditPersonalNotificationRule(EditNotificationRuleMode):
1220 @classmethod
1221 def name(cls):
1222 return "notification_rule_p"
1224 @classmethod
1225 def permissions(cls):
1226 return None
1228 def __init__(self):
1229 super(ModeEditPersonalNotificationRule, self).__init__()
1230 config.user.need_permission("general.edit_notifications")
1232 def _user_id(self):
1233 return config.user.id
1235 def _add_change(self, log_what, log_text):
1236 if config.has_wato_slave_sites():
1237 self._start_async_repl = True
1238 watolib.log_audit(None, log_what, log_text)
1239 else:
1240 super(ModeEditPersonalNotificationRule, self)._add_change(log_what, log_text)
1242 def _back_mode(self):
1243 if config.has_wato_slave_sites():
1244 return
1245 return "user_notifications_p"
1247 def title(self):
1248 if self._new:
1249 return _("Create new notification rule")
1250 return _("Edit notification rule %d") % self._edit_nr
1252 def buttons(self):
1253 html.context_button(_("All Rules"),
1254 watolib.folder_preserving_link([("mode", "user_notifications_p")]), "back")