diff --git a/cmk/plugins/audiocodes/agent_based/fru.py b/cmk/plugins/audiocodes/agent_based/fru.py index 5fb5d0f30cf..65b3eb2bbd0 100644 --- a/cmk/plugins/audiocodes/agent_based/fru.py +++ b/cmk/plugins/audiocodes/agent_based/fru.py @@ -3,7 +3,7 @@ # 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 collections.abc import Mapping from dataclasses import dataclass from cmk.agent_based.v2 import ( @@ -13,13 +13,13 @@ OIDEnd, Result, Service, - SNMPSection, + SimpleSNMPSection, SNMPTree, State, StringTable, ) -from .lib import DETECT_AUDIOCODES +from .lib import data_by_item, DETECT_AUDIOCODES ACTION_MAPPING = { "0": ("Invalid action", State.UNKNOWN), @@ -59,71 +59,80 @@ class FRUModule: status: Status -def parse_audiocodes_fru(string_table: Sequence[StringTable]) -> Mapping[str, FRUModule] | None: - if not all(string_table): +def parse_audiocodes_fru(string_table: StringTable) -> Mapping[str, FRUModule] | None: + if not string_table: return None - name_by_module_index = {module[0]: f"{module[1]} {module[0]}" for module in string_table[0]} - modules_by_index = { + return { module[0]: FRUModule( action=Action(*ACTION_MAPPING[module[1]]), status=Status(*STATUS_MAPPING[module[2]]), ) - for module in string_table[1] + for module in string_table } - return { - name: module - for module_idx, module in modules_by_index.items() - if (name := name_by_module_index.get(module_idx)) - } - -snmp_section_audiocodes_fru = SNMPSection( +snmp_section_audiocodes_fru = SimpleSNMPSection( name="audiocodes_fru", detect=DETECT_AUDIOCODES, - fetch=[ - SNMPTree( - base=".1.3.6.1.2.1.47.1.1.1.1", - oids=[ - OIDEnd(), - "2", - ], - ), - SNMPTree( - base=".1.3.6.1.4.1.5003.9.10.10.4.21.1", - oids=[ - OIDEnd(), - "13", # acSysModuleFRUaction - "14", # acSysModuleFRUstatus - ], - ), - ], + fetch=SNMPTree( + base=".1.3.6.1.4.1.5003.9.10.10.4.21.1", + oids=[ + OIDEnd(), + "13", # acSysModuleFRUaction + "14", # acSysModuleFRUstatus + ], + ), parse_function=parse_audiocodes_fru, ) -def discover_audiocodes_fru(section: Mapping[str, FRUModule]) -> DiscoveryResult: - yield from (Service(item=module_name) for module_name in section) +def discover_audiocodes_fru( + section_audiocodes_module_names: Mapping[str, str] | None, + section_audiocodes_fru: Mapping[str, FRUModule] | None, +) -> DiscoveryResult: + if not section_audiocodes_module_names or not section_audiocodes_fru: + return + + yield from ( + Service(item=item) + for item in data_by_item( + section_audiocodes_module_names, + section_audiocodes_fru, + ) + ) -def check_audiocodes_fru(item: str, section: Mapping[str, FRUModule]) -> CheckResult: - if (module := section.get(item)) is None: +def check_audiocodes_fru( + item: str, + section_audiocodes_module_names: Mapping[str, str] | None, + section_audiocodes_fru: Mapping[str, FRUModule] | None, +) -> CheckResult: + if not section_audiocodes_fru or not section_audiocodes_module_names: + return + + if ( + module_fru := data_by_item( + section_audiocodes_module_names, + section_audiocodes_fru, + ).get(item) + ) is None: return yield Result( - state=module.action.state, - summary=f"Action: {module.action.name}", + state=module_fru.action.state, + summary=f"Action: {module_fru.action.name}", ) yield Result( - state=module.status.state, - summary=f"Status: {module.status.name}", + state=module_fru.status.state, + summary=f"Status: {module_fru.status.name}", ) check_plugin_audiocodes_fru = CheckPlugin( name="audiocodes_fru", service_name="AudioCodes FRU %s", + sections=["audiocodes_module_names", "audiocodes_fru"], discovery_function=discover_audiocodes_fru, check_function=check_audiocodes_fru, ) diff --git a/cmk/plugins/audiocodes/agent_based/lib.py b/cmk/plugins/audiocodes/agent_based/lib.py index 15f68886067..5f0f024347a 100644 --- a/cmk/plugins/audiocodes/agent_based/lib.py +++ b/cmk/plugins/audiocodes/agent_based/lib.py @@ -4,6 +4,22 @@ # conditions defined in the file COPYING, which is part of this source code package. +from collections.abc import Mapping +from typing import TypeVar + from cmk.agent_based.v2 import contains DETECT_AUDIOCODES = contains(".1.3.6.1.2.1.1.2.0", ".1.3.6.1.4.1.5003.8.1.1") + +T = TypeVar("T") + + +def data_by_item( + section_audiocodes_module_names: Mapping[str, str], + data_section: Mapping[str, T], +) -> dict[str, T]: + return { + f"{name} {index}": data + for index, data in data_section.items() + if (name := section_audiocodes_module_names.get(index)) is not None + } diff --git a/cmk/plugins/audiocodes/agent_based/module_names.py b/cmk/plugins/audiocodes/agent_based/module_names.py new file mode 100644 index 00000000000..625795dfab4 --- /dev/null +++ b/cmk/plugins/audiocodes/agent_based/module_names.py @@ -0,0 +1,36 @@ +#!/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 Mapping + +from cmk.agent_based.v2 import ( + OIDEnd, + SimpleSNMPSection, + SNMPTree, + StringTable, +) + +from .lib import DETECT_AUDIOCODES + + +def parse_module_names(string_table: StringTable) -> Mapping[str, str] | None: + if not string_table: + return None + + return {module[0]: module[1] for module in string_table} + + +snmp_section_audiocodes_module_names = SimpleSNMPSection( + name="audiocodes_module_names", + detect=DETECT_AUDIOCODES, + fetch=SNMPTree( + base=".1.3.6.1.2.1.47.1.1.1.1", + oids=[ + OIDEnd(), + "2", + ], + ), + parse_function=parse_module_names, +) diff --git a/cmk/plugins/audiocodes/agent_based/temperature.py b/cmk/plugins/audiocodes/agent_based/temperature.py new file mode 100644 index 00000000000..3f775f0e775 --- /dev/null +++ b/cmk/plugins/audiocodes/agent_based/temperature.py @@ -0,0 +1,100 @@ +#!/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 Mapping + +from cmk.agent_based.v2 import ( + CheckPlugin, + CheckResult, + DiscoveryResult, + OIDEnd, + Result, + Service, + SimpleSNMPSection, + SNMPTree, + State, + StringTable, +) +from cmk.plugins.lib.temperature import check_temperature, TempParamDict + +from .lib import data_by_item, DETECT_AUDIOCODES + + +def parse_audiocodes_temperature(string_table: StringTable) -> Mapping[str, float] | None: + if not string_table: + return None + + return {module[0]: float(module[1]) for module in string_table} + + +snmp_section_audiocodes_temperature = SimpleSNMPSection( + name="audiocodes_temperature", + detect=DETECT_AUDIOCODES, + fetch=SNMPTree( + base=".1.3.6.1.4.1.5003.9.10.10.4.21.1", + oids=[ + OIDEnd(), + "11", # acSysModuleTemperature + ], + ), + parse_function=parse_audiocodes_temperature, +) + + +def discover_audiocodes_temperature( + section_audiocodes_module_names: Mapping[str, str] | None, + section_audiocodes_temperature: Mapping[str, float] | None, +) -> DiscoveryResult: + if not section_audiocodes_module_names or not section_audiocodes_temperature: + return + + yield from ( + Service(item=item) + for item in data_by_item( + section_audiocodes_module_names, + section_audiocodes_temperature, + ) + ) + + +def check_audiocodes_temperature( + item: str, + params: TempParamDict, + section_audiocodes_module_names: Mapping[str, str] | None, + section_audiocodes_temperature: Mapping[str, float] | None, +) -> CheckResult: + if not section_audiocodes_temperature or not section_audiocodes_module_names: + return + + if ( + module_temp := data_by_item( + section_audiocodes_module_names, + section_audiocodes_temperature, + ).get(item) + ) is None: + return + + if module_temp == -1: + yield Result( + state=State.OK, + summary="Temperature is not available", + ) + return + + yield from check_temperature( + reading=module_temp, + params=params, + ) + + +check_plugin_audiocodes_temperature = CheckPlugin( + name="audiocodes_temperature", + service_name="AudioCodes Temperature %s", + sections=["audiocodes_module_names", "audiocodes_temperature"], + discovery_function=discover_audiocodes_temperature, + check_function=check_audiocodes_temperature, + check_default_parameters={}, + check_ruleset_name="temperature", +) diff --git a/cmk/plugins/audiocodes/checkman/audiocodes_temperature b/cmk/plugins/audiocodes/checkman/audiocodes_temperature new file mode 100644 index 00000000000..e9a9dd10063 --- /dev/null +++ b/cmk/plugins/audiocodes/checkman/audiocodes_temperature @@ -0,0 +1,10 @@ +title: AudioCodes: Temperature +agents: snmp +catalog: agentless +license: GPLv2 +distribution: check_mk +description: + This check monitors the temperature of every module in an AudioCodes device. + +discovery: + One service is created for every module. diff --git a/tests/unit/cmk/plugins/audiocodes/agent_based/test_fru.py b/tests/unit/cmk/plugins/audiocodes/agent_based/test_fru.py index fe0e5446db6..85b16727145 100644 --- a/tests/unit/cmk/plugins/audiocodes/agent_based/test_fru.py +++ b/tests/unit/cmk/plugins/audiocodes/agent_based/test_fru.py @@ -3,7 +3,6 @@ # 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 Sequence import pytest @@ -13,22 +12,27 @@ discover_audiocodes_fru, parse_audiocodes_fru, ) +from cmk.plugins.audiocodes.agent_based.module_names import ( + parse_module_names, +) -_STRING_TABLE = [ - [ - ["67387393", "Mediant-4000 Media Processing Module"], - ["67641344", "Mediant-4000 Slot"], - ["67903488", "Mediant-4000 Slot"], - ["67907585", "Mediant-4000 CPU Module"], - ], - [["67387393", "4", "7"], ["67907585", "4", "7"]], +_STRING_TABLE_FRU = [ + ["67387393", "4", "7"], + ["67907585", "4", "7"], +] + +_STRING_TABLE_MODULE_NAMES = [ + ["67387393", "Mediant-4000 Media Processing Module"], + ["67641344", "Mediant-4000 Slot"], + ["67903488", "Mediant-4000 Slot"], + ["67907585", "Mediant-4000 CPU Module"], ] def test_discovery_function() -> None: - section = parse_audiocodes_fru(_STRING_TABLE) - assert section is not None - assert list(discover_audiocodes_fru(section)) == [ + section_fru = parse_audiocodes_fru(_STRING_TABLE_FRU) + section_module_names = parse_module_names(_STRING_TABLE_MODULE_NAMES) + assert list(discover_audiocodes_fru(section_module_names, section_fru)) == [ Service(item="Mediant-4000 Media Processing Module 67387393"), Service(item="Mediant-4000 CPU Module 67907585"), ] @@ -39,13 +43,13 @@ def test_discovery_function() -> None: [ pytest.param( "not_found", - _STRING_TABLE, + _STRING_TABLE_FRU, [], id="Item not found", ), pytest.param( "Mediant-4000 CPU Module 67907585", - _STRING_TABLE, + _STRING_TABLE_FRU, [ Result(state=State.UNKNOWN, summary="Action: Not applicable"), Result(state=State.UNKNOWN, summary="Status: Not applicable"), @@ -55,11 +59,8 @@ def test_discovery_function() -> None: pytest.param( "Mediant-4000 Media Processing Module 67387393", [ - [ - ["67387393", "Mediant-4000 Media Processing Module"], - ["67907585", "Mediant-4000 CPU Module"], - ], - [["67387393", "1", "2"], ["67907585", "4", "7"]], + ["67387393", "1", "2"], + ["67907585", "4", "7"], ], [ Result(state=State.OK, summary="Action: Action done"), @@ -70,11 +71,8 @@ def test_discovery_function() -> None: pytest.param( "Mediant-4000 CPU Module 67907585", [ - [ - ["67387393", "Mediant-4000 Media Processing Module"], - ["67907585", "Mediant-4000 CPU Module"], - ], - [["67387393", "1", "2"], ["67907585", "2", "5"]], + ["67387393", "1", "2"], + ["67907585", "2", "5"], ], [ Result(state=State.WARN, summary="Action: Out of service"), @@ -86,9 +84,9 @@ def test_discovery_function() -> None: ) def test_check_function( item: str, - string_table: Sequence[StringTable], + string_table: StringTable, expected: CheckResult, ) -> None: - section = parse_audiocodes_fru(string_table) - assert section - assert list(check_audiocodes_fru(item, section)) == expected + section_fru = parse_audiocodes_fru(string_table) + section_module_names = parse_module_names(_STRING_TABLE_MODULE_NAMES) + assert list(check_audiocodes_fru(item, section_module_names, section_fru)) == expected diff --git a/tests/unit/cmk/plugins/audiocodes/agent_based/test_temperature.py b/tests/unit/cmk/plugins/audiocodes/agent_based/test_temperature.py new file mode 100644 index 00000000000..0632523678d --- /dev/null +++ b/tests/unit/cmk/plugins/audiocodes/agent_based/test_temperature.py @@ -0,0 +1,85 @@ +#!/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. + + +import pytest + +from cmk.agent_based.v2 import CheckResult, Metric, Result, Service, State +from cmk.plugins.audiocodes.agent_based.module_names import ( + parse_module_names, +) +from cmk.plugins.audiocodes.agent_based.temperature import ( + check_audiocodes_temperature, + discover_audiocodes_temperature, + parse_audiocodes_temperature, +) +from cmk.plugins.lib.temperature import TempParamDict + +_STRING_TABLE_TEMPERATURE = [ + ["67387393", "-1"], + ["67907585", "26"], +] +_STRING_TABLE_MODULE_NAMES = [ + ["67387393", "Mediant-4000 Media Processing Module"], + ["67641344", "Mediant-4000 Slot"], + ["67903488", "Mediant-4000 Slot"], + ["67907585", "Mediant-4000 CPU Module"], +] + + +def test_discovery_function() -> None: + section_temperature = parse_audiocodes_temperature(_STRING_TABLE_TEMPERATURE) + section_module_names = parse_module_names(_STRING_TABLE_MODULE_NAMES) + assert list(discover_audiocodes_temperature(section_module_names, section_temperature)) == [ + Service(item="Mediant-4000 Media Processing Module 67387393"), + Service(item="Mediant-4000 CPU Module 67907585"), + ] + + +@pytest.mark.parametrize( + "item, params, expected", + [ + pytest.param( + "not_found", + {}, + [], + id="Item not found", + ), + pytest.param( + "Mediant-4000 Media Processing Module 67387393", + {}, + [ + Result(state=State.OK, summary="Temperature is not available"), + ], + id="Not applicable temperature", + ), + pytest.param( + "Mediant-4000 CPU Module 67907585", + {"levels": (22.0, 25.0)}, + [ + Metric("temp", 26.0, levels=(22.0, 25.0)), + Result( + state=State.CRIT, summary="Temperature: 26.0 °C (warn/crit at 22.0 °C/25.0 °C)" + ), + Result( + state=State.OK, + notice="Configuration: prefer user levels over device levels (used user levels)", + ), + ], + id="CRIT level temperature", + ), + ], +) +def test_check_function( + item: str, + params: TempParamDict, + expected: CheckResult, +) -> None: + section_temperature = parse_audiocodes_temperature(_STRING_TABLE_TEMPERATURE) + section_module_names = parse_module_names(_STRING_TABLE_MODULE_NAMES) + assert ( + list(check_audiocodes_temperature(item, params, section_module_names, section_temperature)) + == expected + )