From c0ceffb7dd8b7a3ae15521e10738c195595419fd Mon Sep 17 00:00:00 2001 From: Luka Racic Date: Mon, 7 Oct 2024 20:36:49 +0200 Subject: [PATCH] Migrate agent_mqtt to SSC and ruleset APIs CMK-17394 Change-Id: I34a8dee05a8728d94e21518cc906fe432be24867 --- cmk/base/legacy_checks/agent_mqtt.py | 29 ---- cmk/gui/plugins/wato/special_agents/mqtt.py | 110 ------------ cmk/plugins/mqtt/rulesets/special_agent.py | 160 ++++++++++++++++++ .../mqtt/server_side_calls/special_agent.py | 43 +++++ cmk/utils/password_store/hack.py | 1 + tests/unit/checks/test_agent_mqtt.py | 71 -------- .../server_side_calls/test_special_agent.py | 73 ++++++++ .../test_special_agent_args.py | 1 - 8 files changed, 277 insertions(+), 211 deletions(-) delete mode 100644 cmk/base/legacy_checks/agent_mqtt.py delete mode 100644 cmk/gui/plugins/wato/special_agents/mqtt.py create mode 100644 cmk/plugins/mqtt/rulesets/special_agent.py create mode 100644 cmk/plugins/mqtt/server_side_calls/special_agent.py delete mode 100644 tests/unit/checks/test_agent_mqtt.py create mode 100644 tests/unit/cmk/plugins/mqtt/server_side_calls/test_special_agent.py diff --git a/cmk/base/legacy_checks/agent_mqtt.py b/cmk/base/legacy_checks/agent_mqtt.py deleted file mode 100644 index 69dd7785012..00000000000 --- a/cmk/base/legacy_checks/agent_mqtt.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (C) 2021 Checkmk GmbH - License: GNU General Public License v2 -# 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 collections.abc import Mapping, Sequence -from typing import Any - -from cmk.base.check_api import passwordstore_get_cmdline -from cmk.base.config import special_agent_info - - -def agent_mqtt_arguments( - params: Mapping[str, Any], hostname: str, ipaddress: str | None -) -> Sequence[str | tuple[str, str, str]]: - args = [] - for key in sorted(k for k in params if k != "address"): - val = ( - passwordstore_get_cmdline("%s", params[key]) if key == "password" else f"{params[key]}" - ) - args += [f"--{key}", val] - - args += [params.get("address") or ipaddress or hostname] - - return args - - -special_agent_info["mqtt"] = agent_mqtt_arguments diff --git a/cmk/gui/plugins/wato/special_agents/mqtt.py b/cmk/gui/plugins/wato/special_agents/mqtt.py deleted file mode 100644 index 2f85b3ed811..00000000000 --- a/cmk/gui/plugins/wato/special_agents/mqtt.py +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (C) 2019 Checkmk GmbH - License: GNU General Public License v2 -# 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 cmk.utils.rulesets.definition import RuleGroup - -from cmk.gui.i18n import _ -from cmk.gui.valuespec import Dictionary, DropdownChoice, HostAddress, NetworkPort, TextInput -from cmk.gui.wato import IndividualOrStoredPassword, RulespecGroupDatasourceProgramsApps -from cmk.gui.watolib.rulespecs import HostRulespec, rulespec_registry - - -def _valuespec_special_agents_mqtt() -> Dictionary: - return Dictionary( - title=_("MQTT broker statistics"), - help=_( - "Connect to an MQTT broker to get statistics out of your instance. " - "The information is fetched from the $SYS topic of the broker. The " - "different brokers implement different topics as they are not standardized, " - "means that not every service available with every broker. " - "In multi-tentant, enterprise level cluster this agent may not be useful or " - "probably only when directly connecting to single nodes, because the " - "$SYS topic is node-specific." - ), - elements=[ - ( - "username", - TextInput( - title=_("Username"), - help=_("The username used for broker authentication."), - size=32, - allow_empty=False, - ), - ), - ( - "password", - IndividualOrStoredPassword( - title=_("Password of the user"), - allow_empty=False, - ), - ), - ( - "address", - HostAddress( - title=_("Custom address"), - help=_( - "When set, this address is used for connecting to the MQTT " - "broker. If not set, the special agent will use the primary " - "address of the host to connect to the MQTT broker." - ), - size=32, - allow_empty=False, - ), - ), - ( - "port", - NetworkPort( - title=_("Port"), - default_value=1883, - help=_("The port that is used for the api call."), - ), - ), - ( - "client-id", - TextInput( - title=_("Client ID"), - help=_( - "Unique client ID used for the broker. Will be randomly " - "generated when not set." - ), - size=32, - allow_empty=False, - ), - ), - ( - "protocol", - DropdownChoice( - title=_("Protocol"), - choices=[ - ("MQTTv31", "MQTTv31"), - ("MQTTv311", "MQTTv311"), - ("MQTTv5", "MQTTv5"), - ], - default_value="MQTTv311", - ), - ), - ( - "instance-id", - TextInput( - title=_("Instance ID"), - help=_("Unique ID used to identify the instance on the host within Checkmk."), - size=32, - allow_empty=False, - default_value="broker", - ), - ), - ], - required_keys=[], - ) - - -rulespec_registry.register( - HostRulespec( - group=RulespecGroupDatasourceProgramsApps, - name=RuleGroup.SpecialAgents("mqtt"), - valuespec=_valuespec_special_agents_mqtt, - ) -) diff --git a/cmk/plugins/mqtt/rulesets/special_agent.py b/cmk/plugins/mqtt/rulesets/special_agent.py new file mode 100644 index 00000000000..638816b44d6 --- /dev/null +++ b/cmk/plugins/mqtt/rulesets/special_agent.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# Copyright (C) 2019 Checkmk GmbH - License: GNU General Public License v2 +# 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 cmk.utils.hostaddress import HostAddress + +from cmk.rulesets.v1 import Help, Message, Title +from cmk.rulesets.v1.form_specs import ( + DefaultValue, + DictElement, + Dictionary, + Integer, + migrate_to_password, + Password, + SingleChoice, + String, + validators, +) +from cmk.rulesets.v1.form_specs._basic import FieldSize, SingleChoiceElement +from cmk.rulesets.v1.rule_specs import SpecialAgent, Topic + + +def parameter_form() -> Dictionary: + return Dictionary( + title=Title("MQTT broker statistics"), + help_text=Help( + "Connect to an MQTT broker to get statistics out of your instance. " + "The information is fetched from the $SYS topic of the broker. The " + "different brokers implement different topics as they are not standardized, " + "means that not every service available with every broker. " + "In multi-tentant, enterprise level cluster this agent may not be useful or " + "probably only when directly connecting to single nodes, because the " + "$SYS topic is node-specific." + ), + elements={ + "username": DictElement( + required=False, + parameter_form=String( + title=Title("Username"), + help_text=Help("The username used for broker authentication."), + field_size=FieldSize.MEDIUM, + custom_validate=(validators.LengthInRange(min_value=1),), + ), + ), + "password": DictElement( + required=False, + parameter_form=Password( + title=Title("Password of the user"), + custom_validate=(validators.LengthInRange(min_value=1),), + migrate=migrate_to_password, + ), + ), + "address": DictElement( + required=False, + parameter_form=String( + title=Title("Custom address"), + help_text=Help( + "When set, this address is used for connecting to the MQTT " + "broker. If not set, the special agent will use the primary " + "address of the host to connect to the MQTT broker." + ), + field_size=FieldSize.MEDIUM, + custom_validate=( + validators.LengthInRange(min_value=1), + _validate_hostname, + ), + ), + ), + "port": DictElement( + required=False, + parameter_form=Integer( + title=Title("Port"), + prefill=DefaultValue(1883), + help_text=Help("The port that is used for the api call."), + custom_validate=(validators.NetworkPort(),), + ), + ), + "client_id": DictElement( + required=False, + parameter_form=String( + title=Title("Client ID"), + help_text=Help( + "Unique client ID used for the broker. Will be randomly " + "generated when not set." + ), + field_size=FieldSize.MEDIUM, + custom_validate=(validators.LengthInRange(min_value=1),), + ), + ), + "protocol": DictElement( + required=False, + parameter_form=SingleChoice( + title=Title("Protocol"), + elements=[ + SingleChoiceElement(name="MQTTv31", title=Title("MQTTv31")), + SingleChoiceElement(name="MQTTv311", title=Title("MQTTv311")), + SingleChoiceElement(name="MQTTv5", title=Title("MQTTv5")), + ], + prefill=DefaultValue("MQTTv311"), + ), + ), + "instance_id": DictElement( + required=False, + parameter_form=String( + title=Title("Instance ID"), + help_text=Help( + "Unique ID used to identify the instance on the host within Checkmk." + ), + field_size=FieldSize.MEDIUM, + custom_validate=(validators.LengthInRange(min_value=1),), + prefill=DefaultValue("broker"), + ), + ), + }, + ) + + +def _validate_hostname(value: str) -> None: + try: + HostAddress(value) + except ValueError as exception: + raise validators.ValidationError( + message=Message( + "Please enter a valid host name or IPv4 address. " + "Only letters, digits, dash, underscore and dot are allowed." + ) + ) from exception + + +def _migrate_instance_and_client_id(params: object) -> dict[str, object]: + match params: + case {"client-id": client_value, **rest}: + return { + "client_id": client_value, + **{str(k): v for k, v in rest.items()}, + } + case {"instance-id": instance_value, **rest}: + return { + "instance_id": instance_value, + **{str(k): v for k, v in rest.items()}, + } + case {"instance-id": instance_value, "client-id": client_value, **rest}: + return { + "instance_id": instance_value, + "client_id": client_value, + **{str(k): v for k, v in rest.items()}, + } + case dict(): + return {**params} + raise ValueError(f"Invalid parameters: {params!r}") + + +rule_spec_special_agent_mqtt = SpecialAgent( + name="mqtt", + title=Title("MQTT broker statistics"), + topic=Topic.APPLICATIONS, + parameter_form=parameter_form, +) diff --git a/cmk/plugins/mqtt/server_side_calls/special_agent.py b/cmk/plugins/mqtt/server_side_calls/special_agent.py new file mode 100644 index 00000000000..e03c1c130c5 --- /dev/null +++ b/cmk/plugins/mqtt/server_side_calls/special_agent.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# Copyright (C) 2024 Checkmk GmbH - License: GNU General Public License v2 +# 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 collections.abc import Iterable + +from pydantic import BaseModel + +from cmk.server_side_calls.v1 import HostConfig, SpecialAgentCommand, SpecialAgentConfig +from cmk.server_side_calls.v1._utils import Secret + + +class Params(BaseModel): + username: str | None = None + password: Secret | None = None + address: str | None = None + port: int | None = None + client_id: str | None = None + protocol: str | None = None + instance_id: str | None = None + + +def commands_function(params: Params, host_config: HostConfig) -> Iterable[SpecialAgentCommand]: + command_arguments: list[str | Secret] = [] + + command_arguments += ["--client-id", params.client_id] if params.client_id else [] + command_arguments += ["--password", params.password.unsafe()] if params.password else [] + command_arguments += ["--port", f"{params.port}"] if params.port else [] + command_arguments += ["--protocol", params.protocol] if params.protocol else [] + command_arguments += ["--username", params.username] if params.username else [] + + command_arguments += [params.address or host_config.primary_ip_config.address] + + yield SpecialAgentCommand(command_arguments=command_arguments) + + +special_agent_mqtt = SpecialAgentConfig( + name="mqtt", + parameter_parser=Params.model_validate, + commands_function=commands_function, +) diff --git a/cmk/utils/password_store/hack.py b/cmk/utils/password_store/hack.py index af04cc57f29..addf26b766a 100644 --- a/cmk/utils/password_store/hack.py +++ b/cmk/utils/password_store/hack.py @@ -67,6 +67,7 @@ "vsphere": True, "jolokia": True, "random": False, # needs no secret + "mqtt": True, } diff --git a/tests/unit/checks/test_agent_mqtt.py b/tests/unit/checks/test_agent_mqtt.py deleted file mode 100644 index a9484ca63aa..00000000000 --- a/tests/unit/checks/test_agent_mqtt.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (C) 2019 Checkmk GmbH - License: GNU General Public License v2 -# 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 collections.abc import Mapping, Sequence -from typing import Any - -import pytest - -from cmk.utils import password_store - -from .checktestlib import SpecialAgent - -pytestmark = pytest.mark.checks - - -@pytest.mark.parametrize( - "params, expected_result", - [ - pytest.param( - { - "username": "asd", - "password": ("password", "xyz"), - "address": "addr", - "port": 1337, - "client-id": "ding", - "protocol": "MQTTv5", - }, - [ - "--client-id", - "ding", - "--password", - "xyz", - "--port", - "1337", - "--protocol", - "MQTTv5", - "--username", - "asd", - "addr", - ], - id="all_arguments", - ), - pytest.param( - { - "password": ("store", "mqtt_password"), - }, - ["--password", ("store", "mqtt_password", "%s"), "address"], - id="with_password_store", - ), - pytest.param( - {}, - ["address"], - id="minimal_arguments", - ), - ], -) -def test_mqtt_argument_parsing( - params: Mapping[str, Any], - expected_result: Sequence[str], -) -> None: - password_store.save({"mqtt_password": "blablu"}, password_store.password_store_path()) - assert ( - SpecialAgent("agent_mqtt").argument_func( - params, - "testhost", - "address", - ) - == expected_result - ) diff --git a/tests/unit/cmk/plugins/mqtt/server_side_calls/test_special_agent.py b/tests/unit/cmk/plugins/mqtt/server_side_calls/test_special_agent.py new file mode 100644 index 00000000000..e394c05b630 --- /dev/null +++ b/tests/unit/cmk/plugins/mqtt/server_side_calls/test_special_agent.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# Copyright (C) 2019 Checkmk GmbH - License: GNU General Public License v2 +# 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 collections.abc import Mapping + +import pytest + +from cmk.plugins.mqtt.server_side_calls.special_agent import special_agent_mqtt +from cmk.server_side_calls.v1 import HostConfig, IPv4Config, Secret, SpecialAgentCommand + +HOST_CONFIG = HostConfig( + name="host", + ipv4_config=IPv4Config(address="address"), +) + + +@pytest.mark.parametrize( + "raw_params, expected_args", + [ + pytest.param( + { + "username": "asd", + "password": Secret(33), + "address": "addr", + "port": 1337, + "client_id": "ding", + "protocol": "MQTTv5", + }, + SpecialAgentCommand( + command_arguments=[ + "--client-id", + "ding", + "--password", + Secret(33).unsafe(), + "--port", + "1337", + "--protocol", + "MQTTv5", + "--username", + "asd", + "addr", + ] + ), + id="all_arguments", + ), + pytest.param( + { + "password": Secret(id=1, pass_safely=True), + }, + SpecialAgentCommand( + command_arguments=[ + "--password", + Secret(id=1, format="%s", pass_safely=False), + "address", + ] + ), + id="with_password_store", + ), + pytest.param( + {}, + SpecialAgentCommand(command_arguments=["address"]), + id="minimal_arguments", + ), + ], +) +def test_mqtt_argument_parsing( + raw_params: Mapping[str, object], + expected_args: SpecialAgentCommand, +) -> None: + """Tests if all required arguments are present.""" + assert list(special_agent_mqtt(raw_params, HOST_CONFIG)) == [expected_args] diff --git a/tests/unit/cmk/plugins_consistency/test_special_agent_args.py b/tests/unit/cmk/plugins_consistency/test_special_agent_args.py index b6834eaa85a..24893973ed1 100644 --- a/tests/unit/cmk/plugins_consistency/test_special_agent_args.py +++ b/tests/unit/cmk/plugins_consistency/test_special_agent_args.py @@ -108,7 +108,6 @@ UNMIGRATED = { "ipmi_sensors", "jira", - "mqtt", "ruckus_spot", "salesforce", "smb_share",