Skip to content

Commit

Permalink
16155 FIX rule_notifications: allow 3rd party plugins via the REST-API
Browse files Browse the repository at this point in the history
This werk introduces a fix that allows rule notifications to use
3rd party / custom plugins.

CMK-12565
SUP-15947

Change-Id: Iba05bea5369277d72a433caab210f36d64122655
  • Loading branch information
TribeGav committed Oct 17, 2023
1 parent 8cbb09a commit 7f896ed
Show file tree
Hide file tree
Showing 15 changed files with 744 additions and 2,065 deletions.
15 changes: 15 additions & 0 deletions .werks/16155
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Title: rule_notifications: allow 3rd party plugins via the REST-API
Class: fix
Compatible: compat
Component: rest-api
Date: 1696857473
Edition: cre
Knowledge: doc
Level: 1
State: unknown
Version: 2.2.0p13

This werk introduces a fix that allows rule notifications to use
3rd party / custom plugins.


11 changes: 0 additions & 11 deletions cmk/gui/plugins/openapi/endpoints/notification_rules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
NotificationRule,
save_notification_rules,
)
from cmk.gui.watolib.user_scripts import user_script_choices

from cmk import fields

Expand Down Expand Up @@ -77,16 +76,6 @@ def _create_or_update_rule(
detail=_("The rule_id %s does not exist.") % rule_id,
)

plugin_name = incoming_rule_config["notification_method"]["notify_plugin"]["plugin_params"][
"plugin_name"
]
if plugin_name not in [n for (n, _) in user_script_choices("notifications")]:
raise ProblemException(
status=400,
title=_("Plugin doesn't exist"),
detail=_("The plugin '%s' is not a valid notification plugin.") % plugin_name,
)

try:
all_rules[rule_from_request.rule_id] = rule_from_request
save_notification_rules([rule.to_mk_file_format() for rule in all_rules.values()])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
# conditions defined in the file COPYING, which is part of this source code package.

from typing import Any, get_args, Type
import re
from typing import Any, get_args

from marshmallow import post_dump, post_load, ValidationError
from marshmallow import post_dump, post_load, pre_load, ValidationError
from marshmallow.schema import Schema
from marshmallow_oneofschema import OneOfSchema # type: ignore[import]

Expand All @@ -29,6 +30,7 @@
MgmntPriorityType,
MgmntUrgencyType,
OpsGeniePriorityStrType,
PluginOptions,
PushOverPriorityStringType,
RegexModes,
ServiceLevelsStr,
Expand All @@ -41,6 +43,7 @@
INCIDENT_STATE_TYPE,
)

from cmk.gui.exceptions import MKUserError
from cmk.gui.fields import (
ContactGroupField,
FolderIDField,
Expand All @@ -53,6 +56,8 @@
TimePeriodIDField,
)
from cmk.gui.fields.utils import BaseSchema
from cmk.gui.plugins.wato.utils import notification_parameter_registry
from cmk.gui.watolib.user_scripts import user_script_choices

from cmk import fields

Expand All @@ -70,7 +75,7 @@ class CheckboxOneOfSchema(OneOfSchema):
type_field = "state"
type_field_remove = False

def __init__(self, value_schema: Type[Schema], *args: Any, **kwargs: Any) -> None:
def __init__(self, value_schema: type[Schema], *args: Any, **kwargs: Any) -> None:
self.type_schemas = {"disabled": Checkbox, "enabled": value_schema}
super().__init__(*args, **kwargs)

Expand Down Expand Up @@ -1449,7 +1454,7 @@ class PluginName(BaseSchema):
plugin_name = fields.String(
enum=list(get_args(BuiltInPluginNames)),
required=True,
description="The plugin name. Built-in plugins only.",
description="The plugin name.",
example="mail",
)

Expand Down Expand Up @@ -2273,32 +2278,100 @@ class PluginSelector(OneOfSchema):
}


class PluginOption(BaseSchema):
class PluginOptionSchema(BaseSchema):
option = fields.String(
enum=[
"cancel_previous_notifications",
"create_notification_with_the_following_parameters",
PluginOptions.CANCEL.value,
PluginOptions.WITH_PARAMS.value,
PluginOptions.WITH_CUSTOM_PARAMS.value,
],
required=False,
description="Create notifications with parameters or cancel previous notifications",
example="cancel_previous_notifications",
)


class PluginBase(PluginOption):
class PluginBase(PluginOptionSchema):
plugin_params = fields.Nested(
PluginName,
required=True,
)


class PluginWithParams(PluginOption):
class PluginWithParams(PluginOptionSchema):
plugin_params = fields.Nested(
PluginSelector,
required=True,
)


class NonRegisteredCustomPlugin(BaseSchema):
params = fields.List(
fields.String,
required=True,
example=["param1", "param2", "param2"],
)


class CustomPlugin(BaseSchema):
plugin_name = fields.String(
required=True,
description="The custom plugin name",
example="mail",
)

@pre_load
def _pre_load(self, data: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
return {k: v for k, v in data.items() if k in self.fields}

@post_load(pass_original=True)
def _post_load(
self,
data: dict[str, Any],
original_data: dict[str, Any],
**_unused_args: Any,
) -> dict[str, Any]:
dif: dict[str, Any] = {k: v for k, v in original_data.items() if k not in data}
plugin_name = data["plugin_name"]

if plugin_name not in [n for (n, _) in user_script_choices("notifications")]:
raise ValidationError(f"{plugin_name} does not exist")

if plugin_name in notification_parameter_registry:
vs = notification_parameter_registry[data["plugin_name"]]().spec
try:
vs.validate_datatype(dif, "plugin_params")
except MKUserError as exc:
message = exc.message if not ": " in exc.message else exc.message.split(": ")[-1]
if re.search("The entry (.*)", exc.message) is not None:
message = "A required (sub-)field is missing."

raise ValidationError(
message=message,
field_name="_schema" if exc.varname is None else exc.varname.split("_p_")[-1],
)

try:
vs.validate_value(dif, "plugin_params")
except MKUserError as exc:
raise ValidationError(
message=exc.message,
field_name="_schema" if exc.varname is None else exc.varname.split("_p_")[-1],
)

else:
NonRegisteredCustomPlugin().load(dif)

return original_data


class CustomPluginWithParams(PluginOptionSchema):
plugin_params = fields.Nested(
CustomPlugin,
required=True,
)


TIME_HORIZON = fields.Integer(
required=True,
description="Notifications are kept back for bulking at most for this time (seconds)",
Expand Down Expand Up @@ -2406,8 +2479,9 @@ class PluginOptionsSelector(OneOfSchema):
type_field = "option"
type_field_remove = False
type_schemas = {
"cancel_previous_notifications": PluginBase,
"create_notification_with_the_following_parameters": PluginWithParams,
PluginOptions.CANCEL.value: PluginBase,
PluginOptions.WITH_PARAMS.value: PluginWithParams,
PluginOptions.WITH_CUSTOM_PARAMS.value: CustomPluginWithParams,
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

""" Event notification rule api request/response example """

from cmk.utils.type_defs import PluginOptions
from cmk.utils.type_defs.rest_api_types.notifications_rule_types import APINotificationRule


Expand All @@ -19,7 +20,7 @@ def notification_rule_request_example() -> APINotificationRule:
},
"notification_method": {
"notify_plugin": {
"option": "create_notification_with_the_following_parameters",
"option": PluginOptions.WITH_PARAMS.value,
"plugin_params": {
"plugin_name": "mail",
"from_details": {"state": "disabled"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@


from collections.abc import Mapping
from typing import Any, cast, Type
from typing import Any, get_args

from cmk.utils.type_defs import NotificationPluginNameStr
from cmk.utils.type_defs.rest_api_types.notifications_rule_types import APINotifyPlugin
from cmk.utils.type_defs import BuiltInPluginNames, PluginOptions
from cmk.utils.type_defs.rest_api_types.notifications_rule_types import PluginType

from cmk.gui.fields.utils import BaseSchema
from cmk.gui.plugins.openapi.endpoints.notification_rules.common_schemas import (
Expand Down Expand Up @@ -78,42 +78,41 @@ class RulePropertiesAttributes(BaseSchema):
class PluginBase(BaseSchema):
option = fields.String(
enum=[
"cancel_previous_notifications",
"create_notification_with_the_following_parameters",
PluginOptions.CANCEL.value,
PluginOptions.WITH_PARAMS.value,
PluginOptions.WITH_CUSTOM_PARAMS.value,
],
required=True,
)

def dump(self, obj: APINotifyPlugin, *args: Any, **kwargs: Any) -> Mapping:
schema_mapper = cast(
Mapping[NotificationPluginNameStr, Type[BaseSchema]],
{
"mail": HTMLEmailParamsResponse,
"cisco_webex_teams": CiscoWebexPluginResponse,
"mkeventd": MkEventParamsResponse,
"asciimail": AsciiEmailParamsResponse,
"ilert": IlertPluginResponse,
"jira_issues": JiraPluginResponse,
"opsgenie_issues": OpenGeniePluginResponse,
"pagerduty": PagerDutyPluginResponse,
"pushover": PushOverPluginResponse,
"servicenow": ServiceNowPluginResponse,
"signl4": Signl4PluginResponse,
"slack": SlackPluginResponse,
"sms_api": SMSAPIPluginResponse,
"sms": SMSPluginBase,
"spectrum": SpectrumPluginBase,
"victorops": VictoropsPluginResponse,
"msteams": MSTeamsPluginResponse,
},
)

plugin_params = obj["plugin_params"]
plugin_name = plugin_params["plugin_name"]
if schema_to_use := schema_mapper.get(plugin_name):
result = schema_to_use().dump(plugin_params)
obj.update({"plugin_params": result})

def dump(self, obj: dict[str, Any], *args: Any, **kwargs: Any) -> Mapping:
if obj["plugin_params"]["plugin_name"] not in list(get_args(BuiltInPluginNames)):
return obj

schema_mapper: Mapping[BuiltInPluginNames, type[BaseSchema]] = {
"mail": HTMLEmailParamsResponse,
"cisco_webex_teams": CiscoWebexPluginResponse,
"mkeventd": MkEventParamsResponse,
"asciimail": AsciiEmailParamsResponse,
"ilert": IlertPluginResponse,
"jira_issues": JiraPluginResponse,
"opsgenie_issues": OpenGeniePluginResponse,
"pagerduty": PagerDutyPluginResponse,
"pushover": PushOverPluginResponse,
"servicenow": ServiceNowPluginResponse,
"signl4": Signl4PluginResponse,
"slack": SlackPluginResponse,
"sms_api": SMSAPIPluginResponse,
"sms": SMSPluginBase,
"spectrum": SpectrumPluginBase,
"victorops": VictoropsPluginResponse,
"msteams": MSTeamsPluginResponse,
}

plugin_params: PluginType = obj["plugin_params"]
plugin_name: BuiltInPluginNames = plugin_params["plugin_name"]
schema_to_use = schema_mapper[plugin_name]
obj.update({"plugin_params": schema_to_use().dump(plugin_params)})
return obj


Expand Down
2 changes: 1 addition & 1 deletion cmk/gui/wato/pages/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -1560,7 +1560,7 @@ def make_interval_entry() -> list[DictionaryEntry]:
def _notification_script_choices_with_parameters(self):
choices = []
for script_name, title in notification_script_choices():
if script_name in notification_parameter_registry:
if script_name in notification_parameter_registry and script_name is not None:
vs: Dictionary | ListOfStrings = notification_parameter_registry[script_name]().spec
else:
vs = ListOfStrings(
Expand Down
13 changes: 7 additions & 6 deletions cmk/gui/watolib/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,6 @@

import cmk.utils.store as store
from cmk.utils.type_defs import UserId
from cmk.utils.type_defs.notification_plugin_api_types import (
get_plugin_from_api_request,
get_plugin_from_mk_file,
NotificationPlugin,
)
from cmk.utils.type_defs.notify import (
BuiltInPluginNames,
EventRule,
Expand Down Expand Up @@ -63,6 +58,12 @@
MatchServiceLevels,
RestrictToNotificationNumbers,
)
from cmk.utils.type_defs.rest_api_types.notifications_types import (
CustomPlugin,
get_plugin_from_api_request,
get_plugin_from_mk_file,
NotificationPlugin,
)

import cmk.gui.userdb as userdb
from cmk.gui.config import active_config
Expand Down Expand Up @@ -189,7 +190,7 @@ class BulkNotAllowedException(Exception):
@dataclass
class NotificationMethod:
notification_bulking: CheckboxNotificationBulking
notify_plugin: NotificationPlugin
notify_plugin: NotificationPlugin | CustomPlugin

@classmethod
def from_mk_file_format(
Expand Down
Loading

0 comments on commit 7f896ed

Please sign in to comment.