diff --git a/ecall/2.4/local/lib/python3/cmk_addons/plugins/ecall/lib/api.py b/ecall/2.4/local/lib/python3/cmk_addons/plugins/ecall/lib/api.py
new file mode 100644
index 0000000..19d06ed
--- /dev/null
+++ b/ecall/2.4/local/lib/python3/cmk_addons/plugins/ecall/lib/api.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+import logging
+
+import requests
+from urllib3.exceptions import InsecureRequestWarning
+
+
+class DeliveryException(Exception):
+ pass
+
+
+class API(object):
+ """
+ Provide basic access to eCall's HTTP/HTTPS API
+ """
+
+ def __init__(
+ self,
+ baseurl,
+ username,
+ password,
+ address,
+ message,
+ requests_proxy_hosts=None,
+ requests_verify_cert=True,
+ **kwargs,
+ ):
+ self._logger = logging.getLogger("ecall_plugin")
+
+ self._logger.info("Using %s" % self.__class__.__name__)
+
+ # eCall arguments
+ self.baseurl = baseurl
+ self.username = username
+ self.password = password
+ self.address = address
+ self.message = message
+ self.params = kwargs
+
+ # python requests arguments
+ self.proxy_hosts = requests_proxy_hosts
+ self.verify_cert = requests_verify_cert if not requests_proxy_hosts else False
+
+ if not self.verify_cert:
+ # disable InsecureRequestWarning on unverified SSL requests
+ requests.packages.urllib3.disable_warnings(
+ category=InsecureRequestWarning
+ ) # pylint: disable=no-member
+
+ def _get_query_params(self):
+ base = {
+ "Address": self.address,
+ "UserName": self.username,
+ "Password": self.password,
+ "Message": self.message,
+ }
+
+ base.update(self.params)
+
+ if self._logger.isEnabledFor(
+ logging.DEBUG
+ ): # check for log level to not run map on every request
+ # log params but hide password entry
+ self._logger.debug(
+ "Query parameters are: %s",
+ str(dict(map(lambda i: (i[0], "***") if i[0] == "Password" else i, base.items()))),
+ )
+
+ return base
+
+ def send(self):
+ """
+ Send message to eCall API via HTTPS.
+ """
+ self._logger.info("Sending message")
+ self._logger.debug("Using base url %s", self.baseurl)
+ self._logger.debug("Verifying certificates: %s", self.verify_cert)
+ self._logger.debug("Using proxies: %s", self.proxy_hosts)
+
+ if not self.address:
+ self._logger.warning("Got no or empty address information, skipping this notification")
+ else:
+ ret = requests.get(
+ self.baseurl,
+ params=self._get_query_params(),
+ verify=self.verify_cert,
+ proxies=self.proxy_hosts,
+ )
+
+ if ret.status_code == 200:
+ self._logger.info("Successfully delivered message contents to eCall")
+ else:
+ msg = "Unable to deliver message to eCall: %i, Error: %s" % (
+ ret.status_code,
+ ret.text,
+ )
+ self._logger.critical(msg)
+ raise DeliveryException(msg)
+
+
+class SMS(API):
+ def __init__(self, username, password, address, message, **kwargs):
+ super(SMS, self).__init__(
+ "https://url.ecall.ch/api/sms", username, password, address, message, **kwargs
+ )
+
+
+class Voice(API):
+ def __init__(self, username, password, address, message, **kwargs):
+ super(Voice, self).__init__(
+ "https://url.ecall.ch/api/voice", username, password, address, message, **kwargs
+ )
diff --git a/ecall/2.4/local/lib/python3/cmk_addons/plugins/ecall/rulesets/notify_ecall_parameters.py b/ecall/2.4/local/lib/python3/cmk_addons/plugins/ecall/rulesets/notify_ecall_parameters.py
new file mode 100644
index 0000000..909708a
--- /dev/null
+++ b/ecall/2.4/local/lib/python3/cmk_addons/plugins/ecall/rulesets/notify_ecall_parameters.py
@@ -0,0 +1,390 @@
+#!/usr/bin/env python3
+
+from cmk.rulesets.v1.form_specs import (
+ Dictionary,
+ DictElement,
+ String,
+ Integer,
+ Password,
+ BooleanChoice,
+ MultilineText,
+ CascadingSingleChoice,
+ CascadingSingleChoiceElement,
+ SingleChoice,
+ SingleChoiceElement,
+ DefaultValue,
+)
+from cmk.rulesets.v1.rule_specs import (
+ Title,
+ Help,
+ NotificationParameters,
+ Topic,
+)
+from cmk.rulesets.v1.form_specs.validators import (
+ LengthInRange,
+ NumberInRange,
+ NetworkPort,
+ ValidationError,
+)
+
+
+class CallbackLength:
+ def __call__(self, value) -> None:
+ try:
+ # only numeric -> maxlen 16
+ int(value)
+ maxlen = 16
+ errtype = "numeric"
+ except ValueError:
+ # alphanumeric -> maxlen 11
+ maxlen = 11
+ errtype = "alphanumeric"
+
+ if len(value) > maxlen:
+ raise ValidationError(
+ "For callbacks using %s characters the maximum length is %i" % (errtype, maxlen),
+ )
+
+
+def _params_form():
+ return Dictionary(
+ title=Title("Call with the following parameters"),
+ elements={
+ "action": DictElement(
+ required=True,
+ parameter_form=CascadingSingleChoice(
+ title=Title("eCall action"),
+ help_text=Help("Configure the action to take by eCall"),
+ prefill=DefaultValue("sms"),
+ elements=[
+ CascadingSingleChoiceElement(
+ name="sms",
+ title=Title("SMS"),
+ parameter_form=Dictionary(
+ title=Title("Custom parameters"),
+ help_text=Help(
+ "Specify any custom parameters to be sent to the eCall API"
+ ),
+ elements={
+ "JobID": DictElement(
+ parameter_form=String(
+ title=Title("JobID"),
+ custom_validate=[LengthInRange(min_value=1, max_value=50)],
+ ),
+ ),
+ "NotificationAddress": DictElement(
+ parameter_form=String(
+ title=Title("NotificationAddress"),
+ custom_validate=[LengthInRange(min_value=1, max_value=100)],
+ ),
+ ),
+ "NotificationLevel": DictElement(
+ parameter_form=SingleChoice(
+ title=Title("NotificationLevel"),
+ elements=[
+ SingleChoiceElement(
+ name="level_0",
+ title=Title("Confirmation of receipt only when a receipt status is present"),
+ ),
+ SingleChoiceElement(
+ name="level_1",
+ title=Title("Confirmation of receipt once the last possible monitoring point has been reached"),
+ ),
+ SingleChoiceElement(
+ name="level_2",
+ title=Title("Confirmation of receipt once the last possible monitoring point has been reached, and when the job is still not sent after a number of seconds"),
+ ),
+ SingleChoiceElement(
+ name="level_3",
+ title=Title("Send confirmation of receipt when the job could not be sent, i.e. in the case of sending errors or timeout when contacting the end device"),
+ ),
+ ],
+ prefill=DefaultValue("level_0"),
+ ),
+ ),
+ "CallBack": DictElement(
+ parameter_form=String(
+ title=Title("CallBack"),
+ custom_validate=[LengthInRange(min_value=1, max_value=16), CallbackLength()],
+ ),
+ ),
+ "Answer": DictElement(
+ parameter_form=String(
+ title=Title("Answer"),
+ custom_validate=[LengthInRange(min_value=1, max_value=100)],
+ ),
+ ),
+ "MsgType": DictElement(
+ parameter_form=SingleChoice(
+ title=Title("MsgType"),
+ elements=[
+ SingleChoiceElement(name="Normal", title=Title("Normal SMS")),
+ SingleChoiceElement(name="Flash", title=Title("Flash SMS")),
+ SingleChoiceElement(name="PrioSMS", title=Title("Flash and a 'Normal' SMS"))
+ ],
+ prefill=DefaultValue("Normal"),
+ ),
+ ),
+ "NoLog": DictElement(
+ required=True,
+ parameter_form=BooleanChoice(
+ title=Title("Prevent message from showing up in the log book"),
+ prefill=DefaultValue(True),
+ ),
+ ),
+ },
+ ),
+ ),
+ CascadingSingleChoiceElement(
+ name="voice",
+ title=Title("Voice"),
+ parameter_form=Dictionary(
+ title=Title("Custom parameters"),
+ help_text=Help(
+ "Set custom paramters to be sent additionally to the eCall API"
+ ),
+ elements={
+ "JobID": DictElement(
+ parameter_form=String(
+ title=Title("JobID"),
+ custom_validate=[LengthInRange(min_value=1, max_value=50)],
+ )
+ ),
+ "Language": DictElement(
+ parameter_form=SingleChoice(
+ title=Title("Language"),
+ elements=[
+ SingleChoiceElement(name="DE", title=Title("German")),
+ SingleChoiceElement(name="FR", title=Title("French")),
+ SingleChoiceElement(name="IT", title=Title("Italian")),
+ SingleChoiceElement(name="EN", title=Title("English")),
+ ],
+ prefill=DefaultValue("EN"),
+ ),
+ ),
+ "FromText": DictElement(
+ parameter_form=String(
+ title=Title("FromText"),
+ custom_validate=[LengthInRange(min_value=1)],
+ ),
+ ),
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ "username": DictElement(
+ required=True,
+ parameter_form=Password(
+ title=Title("eCall account name"),
+ help_text=Help("The user account used to authenticate against the eCall API."),
+ custom_validate=[LengthInRange(min_value=1)],
+ ),
+ ),
+ "password": DictElement(
+ required=True,
+ parameter_form=Password(
+ title=Title("eCall account password"),
+ help_text=Help("The password to authenticate against eCall"),
+ custom_validate=[LengthInRange(min_value=1)],
+ ),
+ ),
+ "content": DictElement(
+ required=True,
+ parameter_form=Dictionary(
+ title=Title("Message content"),
+ help_text=Help(
+ "Configure the content to be sent to eCall. Not setting any content will result in an empty message."
+ ),
+ elements={
+ "shared": DictElement(
+ required=True,
+ parameter_form=MultilineText(
+ title=Title("Content head for host and service notifications"),
+ help_text=Help(
+ """You may use any checkmk macro here, as well as the following special macros:
+$PARSED_OUTPUT$: Output of either host or service check depending on notification type
+$PARSED_STATE$: State of either host or service depending on notification type
+$PARSED_ITEM$: Item name of the affected monitored object, either hostname/service or service
+"""
+ ),
+ monospaced=True,
+ prefill=DefaultValue("$PARSED_ITEM$ $PARSED_OUTPUT$"),
+ ),
+ ),
+ "host": DictElement(
+ parameter_form=MultilineText(
+ title=Title("Add information in case of host notifications"),
+ monospaced=True,
+ prefill=DefaultValue("""Event: $EVENT_TXT$
+Output: $HOSTOUTPUT$
+Perfdata: $HOSTPERFDATA$
+$LONGHOSTOUTPUT$
+"""),
+ ),
+ ),
+ "service": DictElement(
+ parameter_form=MultilineText(
+ title=Title("Add information in case of service notifications"),
+ monospaced=True,
+ prefill=DefaultValue("""Service: $SERVICEDESC$
+Event: $EVENT_TXT$
+Output: $SERVICEOUTPUT$
+Perfdata: $SERVICEPERFDATA$
+$LONGSERVICEOUTPUT$
+"""),
+ ),
+ ),
+ "bulk_prefix": DictElement(
+ parameter_form=SingleChoice(
+ title=Title("Prefix for bulk notification messages"),
+ help_text=Help(
+ "This setting allows to define a prefix that will be prepended to bulk notification messages."
+ ),
+ elements=[
+ SingleChoiceElement(name="HOSTGROUPNAMES", title=Title("Use host groups as prefix")),
+ SingleChoiceElement(name="SERVICEGROUPNAMES", title=Title("Use service groups as prefix")),
+ ],
+ prefill=DefaultValue("HOSTGROUPNAMES"),
+ ),
+ ),
+ "show_host_if_service": DictElement(
+ required=True,
+ parameter_form=BooleanChoice(
+ title=Title(
+ "Show hostname in PARSED_ITEM for service notifications"
+ ),
+ help_text=Help(
+ "When this option is set, the hostname will be shown in the PARSED_ITEM macro. Otherwise it won't."
+ ),
+ prefill=DefaultValue(True),
+ ),
+ ),
+ },
+ ),
+ ),
+ "content_length_limit": DictElement(
+ required=True,
+ parameter_form=Integer(
+ title=Title("Content length limit"),
+ help_text=Help("Cut the message content after this count of characters"),
+ custom_validate=[NumberInRange(min_value=1, max_value=1530)], # Max value for GSM
+ prefill = DefaultValue(160),
+ ),
+ ),
+ "proxy": DictElement(
+ parameter_form=Dictionary(
+ title=Title("Proxy servers"),
+ help_text=Help(
+ "Proxy servers that should be used when connecting to eCall via HTTPs"
+ ),
+ elements={
+ "host": DictElement(
+ required=True,
+ parameter_form=String(
+ title=Title("Hostname / IP address"),
+ help_text=Help("The hostname or IP address of the proxy system"),
+ custom_validate=[LengthInRange(min_value=1)],
+ ),
+ ),
+ "protocol": DictElement(
+ required=True,
+ parameter_form=SingleChoice(
+ title=Title("Proxy protocol"),
+ elements=[
+ SingleChoiceElement(name="http", title=Title("http")),
+ SingleChoiceElement(name="https", title=Title("https")),
+ ],
+ prefill=DefaultValue("http"),
+ ),
+ ),
+ "port": DictElement(
+ required=True,
+ parameter_form=Integer(
+ title=Title("Port"),
+ help_text=Help("The port the proxy is serving requests on"),
+ custom_validate=[NetworkPort()]
+ ),
+ ),
+ "authentication": DictElement(
+ required=True,
+ parameter_form=Dictionary(
+ title=Title("Authentication"),
+ elements={
+ "user": DictElement(
+ parameter_form=String(
+ title=Title("Username"),
+ help_text=Help(
+ "The username to authenticate on the proxy"
+ ),
+ custom_validate=[LengthInRange(min_value=1)],
+ ),
+ ),
+ "password": DictElement(
+ parameter_form=Password(
+ title=Title("Password"),
+ help_text=Help(
+ "The password to authenticate on the proxy"
+ ),
+ custom_validate=[LengthInRange(min_value=1)],
+ ),
+ ),
+ },
+ ),
+ ),
+ },
+ ),
+ ),
+ "ssl_skip_verify": DictElement(
+ required=True,
+ parameter_form=BooleanChoice(
+ title=Title("Disable SSL certificate verification"),
+ help_text=Help(
+ "Disables verification of SSL certificates. This is useful when behind transparent proxies. When a proxy is configured, this is automatically enabled."
+ ),
+ prefill=DefaultValue(True),
+ ),
+ ),
+ "log_file": DictElement(
+ required=True,
+ parameter_form=String(
+ title=Title("Log file"),
+ help_text=Help("The log file to log to"),
+ custom_validate=[LengthInRange(min_value=1)],
+ prefill=DefaultValue("~/var/log/ecall.log"),
+ ),
+ ),
+ "log_level": DictElement(
+ required=True,
+ parameter_form=SingleChoice(
+ title=Title("Log level"),
+ help_text=Help("Set the minimum log level to show logs for."),
+ elements=[
+ SingleChoiceElement(name="d", title=Title("Debug")),
+ SingleChoiceElement(name="i", title=Title("Info")),
+ SingleChoiceElement(name="w", title=Title("Warning")),
+ SingleChoiceElement(name="c", title=Title("Critical")),
+ ],
+ prefill=DefaultValue("i"),
+ ),
+ ),
+ "stdout": DictElement(
+ required=True,
+ parameter_form=BooleanChoice(
+ title=Title("Log to stdout"),
+ help_text=Help(
+ "Enable log output to stdout. This will be seen as lines prefixed by Output in var/log/notify.log"
+ ),
+ prefill=DefaultValue(True),
+ ),
+ ),
+ },
+ )
+
+rule_spec_tbs_ticket = NotificationParameters(
+ name="eCall",
+ title=Title("eCall – Parameters"),
+ topic=Topic.NOTIFICATIONS,
+ parameter_form=_params_form,
+)
diff --git a/ecall/2.4/local/share/check_mk/notifications/eCall b/ecall/2.4/local/share/check_mk/notifications/eCall
new file mode 100755
index 0000000..306fc8b
--- /dev/null
+++ b/ecall/2.4/local/share/check_mk/notifications/eCall
@@ -0,0 +1,423 @@
+#!/usr/bin/env python3
+# eCall
+# Bulk: yes
+# -*- coding: utf-8 -*-
+"eCall SMS notification plugin for Check_MK monitoring systems using rulebased notifications"
+
+import argparse
+import logging
+import os
+from urllib.parse import quote
+from logging.handlers import TimedRotatingFileHandler
+
+from cmk.notification_plugins.utils import (
+ collect_context,
+ read_bulk_contexts,
+ get_password_from_env_or_context,
+ substitute_context,
+)
+
+from cmk_addons.plugins.ecall.lib.api import SMS, Voice
+
+
+class MessageFormat(object):
+ def __init__(self, shared, service=None, host=None):
+ self._shared = shared
+ self._service = service
+ self._host = host
+
+ @property
+ def service(self):
+ if self._shared and self._service:
+ return "{}\n{}".format(self._shared, self._service)
+ elif self._service: # shared is empty
+ return self._service
+ return self._shared
+
+ @property
+ def host(self):
+ if self._shared and self._host:
+ return "{}\n{}".format(self._shared, self._host)
+ elif self._host: # shared is empty
+ return self._host
+ return self._shared
+
+
+class SettingsException(Exception):
+ pass
+
+
+class Settings(object):
+ def __init__(self, notification_parameters):
+ p = notification_parameters
+ # keys that may just be copied:
+ # username, password, log_file, log_level, stdout
+
+ # handle simple mandatory parameters
+ self.username = get_password_from_env_or_context("USERNAME", p)
+ self.password = get_password_from_env_or_context("PASSWORD", p)
+
+ # in case of using the store, there will be None values when changes have not been activated
+ if self.username is None:
+ raise SettingsException(
+ "Got empty username. Did you activate changes after adding the user to the store?"
+ )
+ if self.password is None:
+ raise SettingsException(
+ "Got empty password. Did you activate changes after adding the password to the store?"
+ )
+
+ self.message_length_limit = int(p["CONTENT_LENGTH_LIMIT"])
+ self.log_file = os.path.expanduser(p["LOG_FILE"])
+ self.log_level = {
+ "d": logging.DEBUG,
+ "i": logging.INFO,
+ "w": logging.WARNING,
+ "c": logging.CRITICAL,
+ }.get(p["LOG_LEVEL"])
+
+ # handle simple optional parameters
+ if "PROXY_HOST" in p:
+ proto = p["PROXY_PROTOCOL"]
+ host = p["PROXY_HOST"]
+ port = p["PROXY_PORT"]
+
+ user = p.get("PROXY_AUTHENTICATION_USER")
+ if user:
+ user = quote(user)
+
+ if p.get("PROXY_AUTHENTICATION_PASSWORD_1"):
+ passwd = get_password_from_env_or_context("PROXY_AUTHENTICATION_PASSWORD", p)
+ passwd = quote(passwd)
+
+ if user and passwd:
+ url = f"{proto}://{user}:{passwd}@{host}:{port}"
+ elif user:
+ url = f"{proto}://{user}@{host}:{port}"
+ else:
+ url = f"{proto}://{host}:{port}"
+
+ self.proxy = {
+ "https": url
+ #proto: url
+ }
+ else:
+ self.proxy = None
+
+ if "SSL_SKIP_VERIFY" in p and p["SSL_SKIP_VERIFY"] == "True":
+ self.ssl_skip_verify = True
+ else:
+ self.ssl_skip_verify = False
+
+ if "STDOUT" in p and p["STDOUT"] == "True":
+ self.log_stdout = True
+ else:
+ self.log_stdout = False
+
+ # handle complex mandatory parameters
+ ## parse message format
+ self.bulk_message_prefix = ""
+ if "CONTENT_BULK_PREFIX" in p:
+ self.bulk_message_prefix = p["CONTENT_BULK_PREFIX"]
+
+ message_format_shared = p["CONTENT_SHARED"]
+ message_format_specifc = {}
+
+ if "CONTENT_HOST" in p:
+ message_format_specifc["host"] = p["CONTENT_HOST"]
+
+ if "CONTENT_SERVICE" in p:
+ message_format_specifc["service"] = p["CONTENT_SERVICE"]
+
+ self.message_format = MessageFormat(message_format_shared, **message_format_specifc)
+
+ ## iterate over every action parameter key in params
+ self.api_class = SMS if p["ACTION_1"] == "sms" else Voice
+ self.api_params = {}
+ for param_key in filter(lambda k: k.startswith("ACTION_2_"), p.keys()):
+ key = param_key.split("_")[2].lower()
+ val = p[param_key]
+ self.api_params[key] = val
+
+ if level := self.api_params.get("notificationlevel"):
+ self.api_params["notificationlevel"] = level.split("_")[1]
+
+ if nolog := self.api_params.get("nolog"):
+ if nolog == "True":
+ self.api_params["nolog"] = 1
+ else:
+ self.api_params["nolog"] = 0
+
+
+class Notification(object):
+ def __init__(self, context, message_format, message_length_limit):
+ self._logger = logging.getLogger("ecall_plugin")
+ self._context = context
+ self._message_format = message_format
+ self._length_limit = message_length_limit
+ self.contact = context["CONTACTPAGER"]
+
+ def _limit(self, text):
+ self._logger.info("Limiting message length to at most %i characters", self._length_limit)
+ return text[: self._length_limit]
+
+ @property
+ def message(self):
+ if self._context["WHAT"] == "SERVICE":
+ message = substitute_context(self._message_format.service, self._context)
+ else:
+ message = substitute_context(self._message_format.host, self._context)
+
+ self._logger.debug("Unlimited message is '%s'", message)
+
+ return self._limit(message)
+
+
+class BulkNotification(Notification):
+ def __init__(self, context_list, message_format, message_length_limit, bulk_message_prefix):
+ # call super to get shared properties
+ # just use the first context as reference for contact
+ # the official plugins also assume that contact is the same for every context
+ super(BulkNotification, self).__init__(
+ context_list[0], message_format, message_length_limit
+ )
+ self._prefix = bulk_message_prefix
+ self._context_list = context_list
+
+ def __format_message(self):
+ prefixes = []
+ affected_hosts = [] # TODO: This is currently not being used
+ service_states = {}
+ host_states = {}
+
+ self._logger.info("Processing bulk context list")
+ for context in self._context_list:
+ self._logger.debug("Processing context %s", context)
+ affected_host = context["HOSTNAME"]
+
+ # keep track of prefixes (if any)
+ if self._prefix:
+ # prefix is either HOSTGROUPNAMES or SERVICEGROUPNAMES
+ # Both are lists of group names separated by whitespace
+ prefixes += context[self._prefix].split(" ")
+
+ # fill list of affected hosts
+ if affected_host not in affected_hosts:
+ affected_hosts.append(affected_host)
+
+ # keep track of states
+ # TODO: We could code logic here to only send the most recent, worst state for each item
+ if context["WHAT"] == "SERVICE":
+ if context["PARSED_STATE"] not in service_states:
+ service_states[context["PARSED_STATE"]] = 1
+ else:
+ service_states[context["PARSED_STATE"]] += 1
+ else:
+ if context["PARSED_STATE"] not in host_states:
+ host_states[context["PARSED_STATE"]] = 1
+ else:
+ host_states[context["PARSED_STATE"]] += 1
+
+ # consolidate prefixes
+ prefixes = list(set(prefixes))
+ prefix_info = ""
+
+ if prefixes:
+ prefix_info = ", ".join(prefixes)
+ self._logger.debug("Got the following prefix for bulk message: %s", prefix_info)
+ prefix_info += ": "
+
+ # generate message
+ self._logger.debug("Raw service states are: %s", service_states)
+ self._logger.debug("Raw host states are: %s", host_states)
+
+ service_info = map(lambda i: "%s: %i" % (i[0].capitalize(), i[1]), service_states.items())
+ host_info = map(lambda i: "%s: %i" % (i[0].capitalize(), i[1]), host_states.items())
+
+ message_items = []
+ if host_info:
+ self._logger.debug("Processed host info is: %s", host_info)
+ message_items.append("Hosts %s" % ", ".join(host_info))
+ if service_info:
+ self._logger.debug("Processed service info is: %s", service_info)
+ message_items.append("Services %s" % ", ".join(service_info))
+
+ return prefix_info + ", ".join(message_items)
+
+ @property
+ def message(self):
+ # use base class when there is only one notification in bulk context
+ if len(self._context_list) == 1:
+ self._logger.info(
+ "Only one context in bulk notification, use default notification method"
+ )
+ return super(BulkNotification, self).message
+
+ return self._limit(self.__format_message())
+
+
+class ContextParser(object):
+ "Parse variable inputs from checkmk notifier"
+
+ def __init__(self):
+ argument_parser = argparse.ArgumentParser(
+ description="Send eCall SMS from checkmk monitoring notifications."
+ )
+ argument_parser.add_argument(
+ "--bulk", help="Enable checkmk bulk mode", action="store_true", default=False
+ )
+ args = argument_parser.parse_args()
+
+ # TODO: maybe this should be configurable in the GUI.
+ # right now, the only notificationtype left is "PROBLEM".
+ # Only for "PROBLEM" the message format is actually being
+ # used. This is not optimal. May be let the user set a
+ # default message format and format and state per type of
+ # notification? This would be more flexible.
+ self.__special_states = {
+ "FLAPPINGSTART": ("WARNING", "started flapping"),
+ "FLAPPINGSTOP": ("OK", "stopped flapping"),
+ "ACKNOWLEDGEMENT": ("OK", "acknowledged"),
+ "RECOVERY": ("OK", "recovered"),
+ "DOWNTIMESTART": ("OK", "downtime started"),
+ "DOWNTIMECANCELLED": ("OK", "downtime cancelled"),
+ "DOWNTIMEEND": ("OK", "downtime ended"),
+ }
+
+ self.bulk = args.bulk
+
+ if args.bulk:
+ self.params, self.context = self.__parse_bulk()
+ else:
+ self.params, self.context = self.__parse_rulebased()
+
+ def __parse_filter_context(self, context, params, is_bulk=False):
+ "Add custom macros to the context and strip out any parameters"
+ what = context["WHAT"]
+ notification_type = context["NOTIFICATIONTYPE"]
+
+ context["PARSED_ITEM"] = context["HOSTNAME"]
+
+ if what == "SERVICE":
+ if params.get("SHOW_HOST_IF_SERVICE", "True") == "True":
+ context["PARSED_ITEM"] = "%s on %s" % (
+ context["SERVICEDESC"],
+ context["PARSED_ITEM"],
+ )
+ else:
+ context["PARSED_ITEM"] = context["SERVICEDESC"]
+
+ if notification_type in self.__special_states:
+ context["PARSED_STATE"], context["PARSED_OUTPUT"] = self.__special_states[
+ notification_type
+ ]
+
+ if is_bulk:
+ # Since we are merely counting the notifications for each state
+ # it makes no sense to keep the original OK/WARN/CRIT/UP/DOWN here.
+ # Instead, we overwrite it with the pretty output (e.g. "started flapping")
+ # so that this is counted in the bulk message.
+ # e.g. Host Started flapping: 1, Downtime ended: 2, Down: 2, Service OK: 3, acknowledged: 4
+ context["PARSED_STATE"] = context["PARSED_OUTPUT"]
+ else:
+ context["PARSED_OUTPUT"] = context["%sOUTPUT" % what]
+ context["PARSED_STATE"] = context["%sSTATE" % what]
+
+ # return new context without parameters (since they would expose passwords)
+ return dict(filter(lambda i: not i[0].startswith("PARAMETER_"), context.items()))
+
+ def __parse_parameters(self, context):
+ params = {}
+ for param_key in filter(lambda k: k.startswith("PARAMETER_"), context.keys()):
+ params[param_key[10::]] = context[param_key]
+
+ return params
+
+ def __parse_rulebased(self):
+ context = collect_context()
+ params = self.__parse_parameters(context)
+ return params, self.__parse_filter_context(context, params)
+
+ def __parse_bulk(self):
+ params, context_list = read_bulk_contexts()
+ params = self.__parse_parameters(params)
+
+ # add custom fields to bulk contexts
+ # when there are no more than 1 contexts in the list, do not use bulk mode (--> is_bulk=False)
+ enriched_contexts = [
+ self.__parse_filter_context(context, params, is_bulk=len(context_list) > 1)
+ for context in context_list
+ ]
+
+ return params, enriched_contexts
+
+
+class Plugin(object):
+ """
+ Check_MK notification plugin to notify events using eCall's HTTP/S API.
+ """
+
+ def __init__(self):
+ parser = ContextParser()
+ settings = Settings(parser.params)
+
+ self._logger = logging.getLogger("ecall_plugin")
+ self.__configure_logging(settings.log_level, settings.log_file, settings.log_stdout)
+
+ self._logger.info("Starting eCall notification process")
+ self._logger.debug("Notification context is %s", parser.context)
+
+ if parser.bulk:
+ notification = BulkNotification(
+ parser.context,
+ settings.message_format,
+ settings.message_length_limit,
+ settings.bulk_message_prefix,
+ )
+ else:
+ notification = Notification(
+ parser.context, settings.message_format, settings.message_length_limit
+ )
+
+ self._logger.debug("Formatted message is %s", notification.message)
+
+ self._logger.debug("Initialising eCall API Object")
+ self.api = settings.api_class(
+ settings.username,
+ settings.password,
+ notification.contact,
+ notification.message,
+ requests_proxy_hosts=settings.proxy,
+ requests_verify_cert=not settings.ssl_skip_verify,
+ **settings.api_params,
+ )
+ self._logger.debug("Initialisation done")
+
+ def __configure_logging(self, log_level, log_file, log_stdout):
+ self._logger.setLevel(log_level)
+ log_format = logging.Formatter("%(asctime)s - %(levelname)s | %(message)s")
+
+ loghandler_file = TimedRotatingFileHandler(
+ log_file, when="midnight", interval=1, backupCount=10
+ )
+ loghandler_file.setFormatter(log_format)
+ loghandler_file.setLevel(log_level)
+
+ self._logger.addHandler(loghandler_file)
+
+ if log_stdout:
+ logger_console = logging.StreamHandler()
+ logger_console.setFormatter(log_format)
+ logger_console.setLevel(log_level)
+ self._logger.addHandler(logger_console)
+
+ def notify(self):
+ """
+ Prepare input data and initiate notification sending
+ """
+ self._logger.info("Notification process started")
+ self.api.send()
+ self._logger.info("Notification process finished")
+
+
+Plugin().notify()
diff --git a/ecall/umb_ecall_notify-2.2.0.mkp b/ecall/umb_ecall_notify-2.2.0.mkp
new file mode 100755
index 0000000..b40c2be
Binary files /dev/null and b/ecall/umb_ecall_notify-2.2.0.mkp differ
diff --git a/ecall/umb_ecall_notify-2.3.0.mkp b/ecall/umb_ecall_notify-2.3.0.mkp
new file mode 100755
index 0000000..31c7b53
Binary files /dev/null and b/ecall/umb_ecall_notify-2.3.0.mkp differ
diff --git a/ecall/umb_ecall_notify-2.4.0.mkp b/ecall/umb_ecall_notify-2.4.0.mkp
new file mode 100755
index 0000000..b496065
Binary files /dev/null and b/ecall/umb_ecall_notify-2.4.0.mkp differ