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",