diff --git a/cmk/plugins/cisco_sma/agent_based/mail_queue.py b/cmk/plugins/cisco_sma/agent_based/mail_queue.py new file mode 100644 index 00000000000..2c9e5cab0ba --- /dev/null +++ b/cmk/plugins/cisco_sma/agent_based/mail_queue.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# Copyright (C) 2025 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 dataclasses import dataclass +from enum import Enum +from typing import assert_never, TypedDict + +from pydantic import BaseModel + +from cmk.agent_based.v2 import ( + check_levels, + CheckPlugin, + CheckResult, + DiscoveryResult, + render, + Result, + Service, + SimpleSNMPSection, + SNMPTree, + State, + StringTable, +) +from cmk.plugins.cisco_sma.agent_based.detect import DETECT_CISCO_SMA_SNMP +from cmk.rulesets.v1.form_specs import SimpleLevelsConfigModel + + +class QueueAvailabilityStatus(Enum): + queue_space_available = 1 + queue_space_shortage = 2 + queue_full = 3 + + +@dataclass(frozen=True) +class Queue(BaseModel): + utilization: float + availability_status: QueueAvailabilityStatus + length: int + oldest_message_age: float + + +def _parse_mail_queue(string_table: StringTable) -> Queue | None: + if not string_table or not string_table[0]: + return None + + data = string_table[0] + + return Queue( + utilization=float(data[0]), + availability_status=QueueAvailabilityStatus(int(data[1])), + length=int(data[2]), + oldest_message_age=float(data[3]), + ) + + +snmp_section_queue = SimpleSNMPSection( + parsed_section_name="cisco_sma_queue", + name="cisco_sma_queue", + detect=DETECT_CISCO_SMA_SNMP, + fetch=SNMPTree( + base=".1.3.6.1.4.1.15497.1.1.1", + oids=["4", "5", "11", "14"], + ), + parse_function=_parse_mail_queue, +) + + +class Params(TypedDict): + monitoring_status_memory_available: int + monitoring_status_memory_shortage: int + monitoring_status_queue_full: int + + monitoring_status_percent_queue_utilization: SimpleLevelsConfigModel[float] + monitoring_status_work_queue_messages: SimpleLevelsConfigModel[int] + monitoring_status_oldest_message_age: SimpleLevelsConfigModel[float] + + +def _check_mail_queue(params: Params, section: Queue) -> CheckResult: + match section.availability_status: + case QueueAvailabilityStatus.queue_space_available: + yield Result( + state=State(params["monitoring_status_memory_available"]), + notice="Memory available", + ) + case QueueAvailabilityStatus.queue_space_shortage: + yield Result( + state=State(params["monitoring_status_memory_shortage"]), + notice="Memory shortage", + ) + case QueueAvailabilityStatus.queue_full: + yield Result(state=State(params["monitoring_status_queue_full"]), notice="Memory full") + case _: + assert_never(section.availability_status) + + yield from check_levels( + section.utilization, + label="Utilization", + render_func=render.percent, + metric_name="cisco_sma_queue_utilization", + levels_upper=params["monitoring_status_percent_queue_utilization"], + ) + + yield from check_levels( + section.length, + label="Total messages", + metric_name="cisco_sma_queue_length", + levels_upper=params["monitoring_status_work_queue_messages"], + notice_only=True, + ) + + yield from check_levels( + section.oldest_message_age, + label="Oldest message age", + metric_name="cisco_sma_queue_oldest_message_age", + levels_upper=params["monitoring_status_oldest_message_age"], + render_func=render.timespan, + notice_only=True, + ) + + +def _discover_queue(section: Queue) -> DiscoveryResult: + yield Service() + + +check_plugin_queue = CheckPlugin( + name="cisco_sma_queue", + service_name="Queue", + discovery_function=_discover_queue, + check_function=_check_mail_queue, + check_ruleset_name="cisco_sma_queue", + check_default_parameters=Params( + monitoring_status_memory_available=State.OK.value, + monitoring_status_memory_shortage=State.WARN.value, + monitoring_status_queue_full=State.CRIT.value, + monitoring_status_percent_queue_utilization=("fixed", (80.0, 90.0)), + monitoring_status_work_queue_messages=("fixed", (500, 1000)), + monitoring_status_oldest_message_age=("no_levels", None), + ), +) diff --git a/cmk/plugins/cisco_sma/checkman/cisco_sma_queue b/cmk/plugins/cisco_sma/checkman/cisco_sma_queue new file mode 100644 index 00000000000..2c32e9fbe16 --- /dev/null +++ b/cmk/plugins/cisco_sma/checkman/cisco_sma_queue @@ -0,0 +1,22 @@ +title: Cisco SMA Mail Queue +agents: snmp +catalog: os/storage +license: GPLv2 +distribution: check_mk +description: + This check monitors the mail queue for the Cisco Security Management Appliance (SMA). + + The mapping of memory availability statuses reported by the system can be configured via the dedicated ruleset. + + The check utilizes the following OID from ASYNCOS-MAIL-MIB + + - queueAvailabilityStatus .1.3.6.1.4.1.15497.1.1.1.5 + + - workQueueMessages .1.3.6.1.4.1.15497.1.1.1.11 + + - oldestMessageAge .1.3.6.1.4.1.15497.1.1.1.14 + + - perCentQueueUtilization .1.3.6.1.4.1.15497.1.1.1.4 + +discovery: + One service is created. \ No newline at end of file diff --git a/cmk/plugins/cisco_sma/graphing/mail_queue_standalone.py b/cmk/plugins/cisco_sma/graphing/mail_queue_standalone.py new file mode 100644 index 00000000000..f0d17df63b2 --- /dev/null +++ b/cmk/plugins/cisco_sma/graphing/mail_queue_standalone.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# Copyright (C) 2025 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.graphing.v1 import metrics, Title + +UNIT_SECOND = metrics.Unit(metrics.TimeNotation()) +UNIT_INTEGER = metrics.Unit(metrics.DecimalNotation("")) + + +metric_queue_length = metrics.Metric( + name="cisco_sma_queue_length", + title=Title("Queue Length"), + unit=UNIT_INTEGER, + color=metrics.Color.DARK_GREEN, +) + +metric_queue_oldest_message_age = metrics.Metric( + name="cisco_sma_queue_oldest_message_age", + title=Title("Oldest Message Age"), + unit=UNIT_SECOND, + color=metrics.Color.DARK_ORANGE, +) diff --git a/cmk/plugins/cisco_sma/graphing/mail_queue_utilization.py b/cmk/plugins/cisco_sma/graphing/mail_queue_utilization.py new file mode 100644 index 00000000000..337e7201e7d --- /dev/null +++ b/cmk/plugins/cisco_sma/graphing/mail_queue_utilization.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# Copyright (C) 2025 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.graphing.v1 import metrics, perfometers, Title + +UNIT_PERCENT = metrics.Unit(metrics.DecimalNotation("%")) + +metric_queue_utilization = metrics.Metric( + name="cisco_sma_queue_utilization", + title=Title("Utilization"), + unit=UNIT_PERCENT, + color=metrics.Color.DARK_BLUE, +) + +perfometer_queue_utilization = perfometers.Perfometer( + name="cisco_sma_queue_utilization_perfometer", + focus_range=perfometers.FocusRange( + lower=perfometers.Closed(0), + upper=perfometers.Open(100), + ), + segments=["cisco_sma_queue_utilization"], +) diff --git a/cmk/plugins/cisco_sma/rulesets/mail_queue.py b/cmk/plugins/cisco_sma/rulesets/mail_queue.py new file mode 100644 index 00000000000..91e04d91860 --- /dev/null +++ b/cmk/plugins/cisco_sma/rulesets/mail_queue.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# Copyright (C) 2025 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.rulesets.v1 import Label, Title +from cmk.rulesets.v1.form_specs import ( + DefaultValue, + DictElement, + Dictionary, + InputHint, + Integer, + LevelDirection, + LevelsType, + Percentage, + ServiceState, + SimpleLevels, + SimpleLevelsConfigModel, + TimeMagnitude, + TimeSpan, +) +from cmk.rulesets.v1.rule_specs import CheckParameters, HostCondition, Topic + + +def _queue_form() -> Dictionary: + return Dictionary( + elements={ + "monitoring_status_memory_available": DictElement( + parameter_form=ServiceState( + title=Title("Monitoring status if memory is available"), + prefill=DefaultValue(ServiceState.OK), + ) + ), + "monitoring_status_memory_shortage": DictElement( + parameter_form=ServiceState( + title=Title("Monitoring status in case of memory shortage"), + prefill=DefaultValue(ServiceState.WARN), + ) + ), + "monitoring_status_queue_full": DictElement( + parameter_form=ServiceState( + title=Title("Monitoring status if memory is full"), + prefill=DefaultValue(ServiceState.CRIT), + ) + ), + "monitoring_status_percent_queue_utilization": DictElement[ + SimpleLevelsConfigModel[float] + ]( + required=True, + parameter_form=SimpleLevels( + title=Title("Levels on queue utilization"), + level_direction=LevelDirection.UPPER, + form_spec_template=Percentage(), + prefill_fixed_levels=InputHint((80.0, 90.0)), + ), + ), + "monitoring_status_work_queue_messages": DictElement[SimpleLevelsConfigModel[int]]( + required=True, + parameter_form=SimpleLevels( + title=Title("Levels on total number of messages in queue"), + level_direction=LevelDirection.UPPER, + form_spec_template=Integer(), + prefill_fixed_levels=InputHint((500, 1000)), + ), + ), + "monitoring_status_oldest_message_age": DictElement[SimpleLevelsConfigModel[float]]( + required=True, + parameter_form=SimpleLevels( + title=Title("Levels on age of oldest message"), + level_direction=LevelDirection.UPPER, + form_spec_template=TimeSpan( + label=Label(""), + displayed_magnitudes=[ + TimeMagnitude.HOUR, + TimeMagnitude.MINUTE, + TimeMagnitude.SECOND, + ], + prefill=DefaultValue(58.0), + ), + prefill_levels_type=DefaultValue(LevelsType.NONE), + prefill_fixed_levels=InputHint((0.0, 0.0)), + ), + ), + } + ) + + +rule_spec_queue = CheckParameters( + name="cisco_sma_queue", + title=Title("Cisco SMA queue"), + topic=Topic.APPLICATIONS, + parameter_form=_queue_form, + condition=HostCondition(), +) diff --git a/tests/unit/cmk/plugins/cisco_sma/agent_based/test_dns.py b/tests/unit/cmk/plugins/cisco_sma/agent_based/test_dns.py index aa1c9043018..62a802987a0 100644 --- a/tests/unit/cmk/plugins/cisco_sma/agent_based/test_dns.py +++ b/tests/unit/cmk/plugins/cisco_sma/agent_based/test_dns.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (C) 2024 Checkmk GmbH - License: GNU General Public License v2 +# Copyright (C) 2025 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. diff --git a/tests/unit/cmk/plugins/cisco_sma/agent_based/test_mail_queue.py b/tests/unit/cmk/plugins/cisco_sma/agent_based/test_mail_queue.py new file mode 100644 index 00000000000..1848dc24cf5 --- /dev/null +++ b/tests/unit/cmk/plugins/cisco_sma/agent_based/test_mail_queue.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +# Copyright (C) 2025 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. + + +import pytest + +from cmk.agent_based.v2 import ( + CheckResult, + Metric, + Result, + Service, + State, + StringTable, +) +from cmk.plugins.cisco_sma.agent_based.mail_queue import ( + _check_mail_queue, + _discover_queue, + _parse_mail_queue, + Params, +) + + +@pytest.mark.parametrize( + "string_table", + [ + ([["10", "2", "30", "40"]]), + ], +) +def test_discover_queue(string_table: StringTable) -> None: + queue = _parse_mail_queue(string_table) + assert queue is not None + assert list(_discover_queue(queue)) == [Service()] + + +@pytest.mark.parametrize( + "params, string_table, expected", + ( + ( + Params( + monitoring_status_memory_available=State.OK.value, + monitoring_status_memory_shortage=State.WARN.value, + monitoring_status_queue_full=State.CRIT.value, + monitoring_status_percent_queue_utilization=("fixed", (80.0, 90.0)), + monitoring_status_work_queue_messages=("fixed", (500, 1000)), + monitoring_status_oldest_message_age=("no_levels", None), + ), + [["10", "2", "300", "400"]], + [ + Result(state=State.WARN, summary="Memory shortage"), + Result(state=State.OK, summary="Utilization: 10.00%"), + Metric("cisco_sma_queue_utilization", 10.0, levels=(80.0, 90.0)), + Result(state=State.OK, notice="Total messages: 300.00"), + Metric("cisco_sma_queue_length", 300.0, levels=(500.0, 1000.0)), + Result(state=State.OK, notice="Oldest message age: 6 minutes 40 seconds"), + Metric("cisco_sma_queue_oldest_message_age", 400.0), + ], + ), + ( + Params( + monitoring_status_memory_available=State.OK.value, + monitoring_status_memory_shortage=State.WARN.value, + monitoring_status_queue_full=State.CRIT.value, + monitoring_status_percent_queue_utilization=("fixed", (80.0, 90.0)), + monitoring_status_work_queue_messages=("fixed", (500, 1000)), + monitoring_status_oldest_message_age=("fixed", (3000, 3600)), + ), + [["85", "2", "700", "3600"]], + [ + Result(state=State.WARN, summary="Memory shortage"), + Result( + state=State.WARN, summary="Utilization: 85.00% (warn/crit at 80.00%/90.00%)" + ), + Metric("cisco_sma_queue_utilization", 85.0, levels=(80.0, 90.0)), + Result( + state=State.WARN, summary="Total messages: 700.00 (warn/crit at 500.00/1000.00)" + ), + Metric("cisco_sma_queue_length", 700.0, levels=(500.0, 1000.0)), + Result( + state=State.CRIT, + summary="Oldest message age: 1 hour 0 minutes (warn/crit at 50 minutes 0 seconds/1 hour 0 minutes)", + ), + Metric("cisco_sma_queue_oldest_message_age", 3600.0, levels=(3000.0, 3600.0)), + ], + ), + ), +) +def test_check_mail_queue( + params: Params, + string_table: StringTable, + expected: CheckResult, +) -> None: + queue = _parse_mail_queue(string_table) + assert queue is not None + assert list(_check_mail_queue(params, queue)) == list(expected)