From 2b522d255397a0fe4a569284ad3aeaab76894ea8 Mon Sep 17 00:00:00 2001 From: George Pochiscan Date: Tue, 19 Jul 2022 11:42:36 +0300 Subject: [PATCH] Added checks for Avocent ACS800 Devices for checkmk 2.0 and 2.1 --- check_mk-avocent/cmk20-cmk21/Avocent-1.1.mkp | Bin 0 -> 3938 bytes .../base/plugins/agent_based/avocent_psu.py | 92 +++++++++++ .../plugins/agent_based/avocent_sensors.py | 134 +++++++++++++++ .../agent_based/utils/ucd_hr_detection.py | 152 ++++++++++++++++++ .../check_mk/checkman/avocent_sensors_temp | 15 ++ .../check_mk/checkman/avocent_voltage_sensors | 15 ++ 6 files changed, 408 insertions(+) create mode 100644 check_mk-avocent/cmk20-cmk21/Avocent-1.1.mkp create mode 100644 check_mk-avocent/cmk20-cmk21/lib/python3/cmk/base/plugins/agent_based/avocent_psu.py create mode 100644 check_mk-avocent/cmk20-cmk21/lib/python3/cmk/base/plugins/agent_based/avocent_sensors.py create mode 100644 check_mk-avocent/cmk20-cmk21/lib/python3/cmk/base/plugins/agent_based/utils/ucd_hr_detection.py create mode 100644 check_mk-avocent/cmk20-cmk21/share/check_mk/checkman/avocent_sensors_temp create mode 100644 check_mk-avocent/cmk20-cmk21/share/check_mk/checkman/avocent_voltage_sensors diff --git a/check_mk-avocent/cmk20-cmk21/Avocent-1.1.mkp b/check_mk-avocent/cmk20-cmk21/Avocent-1.1.mkp new file mode 100644 index 0000000000000000000000000000000000000000..5901d30445276fe14f8600176c79252ffdcc4aaf GIT binary patch literal 3938 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 X`TxG~f0-{O7@W}BqFk(8EUf Section: + return string_table[0][0] + + +register.snmp_section( + name="avocent_psu", + detect=startswith(".1.3.6.1.2.1.1.1.0", "Avocent"), + parse_function=parse_avocent_psu, + fetch=[ + SNMPTree( + base=".1.3.6.1.4.1.10418.26.2.1.8", + oids=[ + "1", #Number of PSU installed + "2", #PowerSupply1 state + "3", #PowerSupply2 state + ], + ), + ], +) + +def discovery_avocent_psu(section: Section) -> DiscoveryResult: + yield Service() + + +def _power_supply_status_descr(status_nr: str) -> str: + return { + "1": "Powered On", + "2": "Powered Off", + "9999": "Power Supply is not installed", + }.get(status_nr, status_nr) + +def _power_supply_state(status_nr: str) -> State: + return { + "1": State.OK, + "2": State.CRIT, + "9999": State.OK + }.get(status_nr, State.UNKNOWN) + + + +def check_avocent_psu( + section: Section, +) -> CheckResult: + number_of_psu=section[0] + state_psu_1=section[1] + state_psu_2=section[2] + + yield Result( + state=State.OK, + summary="Number of PSU installed: %s" % number_of_psu, + ) + + yield Result( + state=_power_supply_state(state_psu_1), + summary="Power Supply 1 is %s" % _power_supply_status_descr(state_psu_1), + ) + + yield Result( + state=_power_supply_state(state_psu_2), + summary="Power Supply 2 is %s" % _power_supply_status_descr(state_psu_2), + ) + + +register.check_plugin( + name="avocent_psu", + sections=["avocent_psu"], + service_name="Power Supplies", + discovery_function=discovery_avocent_psu, + check_function=check_avocent_psu +) + diff --git a/check_mk-avocent/cmk20-cmk21/lib/python3/cmk/base/plugins/agent_based/avocent_sensors.py b/check_mk-avocent/cmk20-cmk21/lib/python3/cmk/base/plugins/agent_based/avocent_sensors.py new file mode 100644 index 0000000..4525f42 --- /dev/null +++ b/check_mk-avocent/cmk20-cmk21/lib/python3/cmk/base/plugins/agent_based/avocent_sensors.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright (C) 2019 tribe29 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 dataclasses +from typing import Mapping + +from .agent_based_api.v1 import ( + contains, + get_value_store, + Metric, + register, + Result, + Service, + SNMPTree, + State, + startswith +) +from .agent_based_api.v1.type_defs import CheckResult, DiscoveryResult, StringTable +from .utils.temperature import check_temperature, TempParamType + +@dataclasses.dataclass(frozen=True) +class Sensor: + value: float + + +@dataclasses.dataclass(frozen=True) +class VoltageSensor(Sensor): + ... + + +@dataclasses.dataclass(frozen=True) +class Section: + temperature_sensors: Mapping[str, Sensor] + voltage_sensors: Mapping[str, Sensor] + +temperature_sensors_name = ['CPU','Board'] +voltage_sensors_name = ['PSU 1','PSU 2'] + +def parse_avocent_sensors(string_table: StringTable) -> Section: + temperature_sensors = {} + voltage_sensors = {} + position = 0 + for temp_sens_name in temperature_sensors_name: + temperature_sensors[temp_sens_name] = Sensor(value=int(string_table[0][position])) + position +=1 + + pos = 2 + for volt_sens_name in voltage_sensors_name: + voltage_sensors[volt_sens_name] = Sensor(value=float(string_table[0][pos])/100) + pos += 1 + + return Section( + temperature_sensors=temperature_sensors, + voltage_sensors=voltage_sensors, + ) + +register.snmp_section( + name="avocent_sensors", + detect=startswith(".1.3.6.1.2.1.1.1.0", "Avocent"), + parse_function=parse_avocent_sensors, + fetch=SNMPTree( + base=".1.3.6.1.4.1.10418.26.2.7", + oids=[ + "1", #acsSensorsInternalCurrentCPUTemperature + "6", #acsSensorsInternalCurrentBoardTemperature + "17", #acsSensorsVoltagePowerSupply1 + "18", #acsSensorsVoltagePowerSupply2 + ], + ), +) + + + +def discover_avocent_voltage_sensors(section: Section) -> DiscoveryResult: + yield from (Service(item=sensor_name) for sensor_name in section.voltage_sensors) + + +def check_avocent_voltage_sensors( + item: str, + section: Section, +) -> CheckResult: + if not (sensor := section.voltage_sensors.get(item)): + return + + yield Result( + state=State.OK, + summary=f"{sensor.value:.1f} V", + ) + + yield Metric( + name="voltage", + value=sensor.value, + ) + + +register.check_plugin( + name="avocent_voltage_sensors", + sections=["avocent_sensors"], + service_name="Voltage %s", + discovery_function=discover_avocent_voltage_sensors, + check_function=check_avocent_voltage_sensors, +) + + +def discover_avocent_sensors_temp(section: Section) -> DiscoveryResult: + yield from (Service(item=sensor_name) for sensor_name in section.temperature_sensors) + + +def check_avocent_sensors_temp( + item: str, + params: TempParamType, + section: Section, +) -> CheckResult: + if not (sensor := section.temperature_sensors.get(item)): + return + yield from check_temperature( + reading=sensor.value, + params=params, + unique_name=item, + value_store=get_value_store(), + ) + +register.check_plugin( + name="avocent_sensors_temp", + sections=["avocent_sensors"], + service_name="Temperature %s", + discovery_function=discover_avocent_sensors_temp, + check_function=check_avocent_sensors_temp, + check_ruleset_name="temperature", + check_default_parameters={"device_levels_handling": "devdefault"}, +) diff --git a/check_mk-avocent/cmk20-cmk21/lib/python3/cmk/base/plugins/agent_based/utils/ucd_hr_detection.py b/check_mk-avocent/cmk20-cmk21/lib/python3/cmk/base/plugins/agent_based/utils/ucd_hr_detection.py new file mode 100644 index 0000000..3d63f78 --- /dev/null +++ b/check_mk-avocent/cmk20-cmk21/lib/python3/cmk/base/plugins/agent_based/utils/ucd_hr_detection.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright (C) 2019 tribe29 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 ..agent_based_api.v1 import ( + all_of, + any_of, + contains, + equals, + exists, + not_contains, + not_equals, + not_exists, + not_startswith, + startswith, +) + +# We are not sure how to safely detect the UCD SNMP Daemon. We know that +# it is mainly used on Linux, but not only. But fetching and OID outside +# of the info area for scanning is not a good idea. It will slow down +# scans for *all* hosts. + +# ---ucd cpu load--------------------------------------------------------- + +# We prefer HOST-RESOURCES-MIB implementation but not in case +# of check 'ucd_cpu_load' because the HR-MIB has not data +# about cpu load + +# ---general ucd/hr------------------------------------------------------- + +HR = exists(".1.3.6.1.2.1.25.1.1.0") + +_NOT_HR = not_exists(".1.3.6.1.2.1.25.1.1.0") + +UCD = any_of( + contains(".1.3.6.1.2.1.1.1.0", "linux"), + contains(".1.3.6.1.2.1.1.1.0", "cmc-tc"), + contains(".1.3.6.1.2.1.1.1.0", "hp onboard administrator"), + contains(".1.3.6.1.2.1.1.1.0", "barracuda"), + contains(".1.3.6.1.2.1.1.1.0", "pfsense"), + contains(".1.3.6.1.2.1.1.1.0", "genugate"), + contains(".1.3.6.1.2.1.1.1.0", "bomgar"), + contains(".1.3.6.1.2.1.1.1.0", "pulse secure"), + contains(".1.3.6.1.2.1.1.1.0", "microsens"), + contains(".1.3.6.1.2.1.1.1.0", "avocent"), + all_of( # Artec email archive appliances + equals(".1.3.6.1.2.1.1.2.0", ".1.3.6.1.4.1.8072.3.2.10"), + contains(".1.3.6.1.2.1.1.1.0", "version"), + contains(".1.3.6.1.2.1.1.1.0", "serial"), + ), + all_of( + equals(".1.3.6.1.2.1.1.1.0", ""), + exists(".1.3.6.1.4.1.2021.*"), + ), +) + +_NOT_UCD = all_of( + # This is an explicit negation of the constant above. + # We don't have a generic negation function as we want + # discourage constructs like this. + # In the future this will be acomplished using the 'supersedes' + # feature (according to CMK-4232), and this can be removed. + not_contains(".1.3.6.1.2.1.1.1.0", "linux"), + not_contains(".1.3.6.1.2.1.1.1.0", "cmc-tc"), + not_contains(".1.3.6.1.2.1.1.1.0", "hp onboard administrator"), + not_contains(".1.3.6.1.2.1.1.1.0", "barracuda"), + not_contains(".1.3.6.1.2.1.1.1.0", "pfsense"), + not_contains(".1.3.6.1.2.1.1.1.0", "genugate"), + not_contains(".1.3.6.1.2.1.1.1.0", "bomgar"), + not_contains(".1.3.6.1.2.1.1.1.0", "pulse secure"), + not_contains(".1.3.6.1.2.1.1.1.0", "microsens"), + not_contains(".1.3.6.1.2.1.1.1.0", "avocent"), + any_of( # Artec email archive appliances + not_equals(".1.3.6.1.2.1.1.2.0", ".1.3.6.1.4.1.8072.3.2.10"), + not_contains(".1.3.6.1.2.1.1.1.0", "version"), + not_contains(".1.3.6.1.2.1.1.1.0", "serial"), + ), +) + +PREFER_HR_ELSE_UCD = all_of(UCD, _NOT_HR) + +# ---helper--------------------------------------------------------------- + +# Within _is_ucd or _is_ucd_mem we make use of a whitelist +# in order to expand this list of devices easily. + +_UCD_MEM = any_of( + # Devices for which ucd_mem should be used + # if and only if HR-table is not available + all_of( + contains(".1.3.6.1.2.1.1.1.0", "pfsense"), + not_exists(".1.3.6.1.2.1.25.1.1.0"), + ), + all_of( + contains(".1.3.6.1.2.1.1.1.0", "ironport model c3"), + not_exists(".1.3.6.1.2.1.25.1.1.0"), + ), + all_of( + contains(".1.3.6.1.2.1.1.1.0", "bomgar"), + not_exists(".1.3.6.1.2.1.25.1.1.0"), + ), + all_of( + # Astaro and Synology are Linux but should use hr_mem + # Otherwise Cache/Buffers are included in used memory + # generating critical state + not_startswith(".1.3.6.1.2.1.1.2.0", ".1.3.6.1.4.1.8072."), + # Otherwise use ucd_mem for listed devices in UCD. + UCD, + ), +) + +_NOT_UCD_MEM = all_of( + # This is an explicit negation of the constant above. + # We don't have a generic negation function as we want + # discourage constructs like this. + # In the future this will be acomplished using the 'supersedes' + # feature (according to CMK-4232), and this can be removed. + any_of( + not_contains(".1.3.6.1.2.1.1.1.0", "pfsense"), + exists(".1.3.6.1.2.1.25.1.1.0"), + ), + any_of( + not_contains(".1.3.6.1.2.1.1.1.0", "ironport model c3"), + exists(".1.3.6.1.2.1.25.1.1.0"), + ), + any_of( + not_contains(".1.3.6.1.2.1.1.1.0", "bomgar"), + exists(".1.3.6.1.2.1.25.1.1.0"), + ), + any_of( + # Astaro and Synology are Linux but should use hr_mem + # Otherwise Cache/Buffers are included in used memory + # generating critical state + startswith(".1.3.6.1.2.1.1.2.0", ".1.3.6.1.4.1.8072."), + # Otherwise use ucd_mem for listed devices in UCD. + _NOT_UCD, + ), +) + +# Some devices report incorrect data on both HR and UCD, eg. F5 BigIP +_NOT_BROKEN_MEM = all_of( + not_startswith(".1.3.6.1.2.1.1.2.0", ".1.3.6.1.4.1.3375"), + not_startswith(".1.3.6.1.2.1.1.2.0", ".1.3.6.1.4.1.2620"), +) + +# ---memory--------------------------------------------------------------- + +USE_UCD_MEM = all_of(_NOT_BROKEN_MEM, _UCD_MEM) + +USE_HR_MEM = all_of(_NOT_BROKEN_MEM, _NOT_UCD_MEM) diff --git a/check_mk-avocent/cmk20-cmk21/share/check_mk/checkman/avocent_sensors_temp b/check_mk-avocent/cmk20-cmk21/share/check_mk/checkman/avocent_sensors_temp new file mode 100644 index 0000000..12d2ad3 --- /dev/null +++ b/check_mk-avocent/cmk20-cmk21/share/check_mk/checkman/avocent_sensors_temp @@ -0,0 +1,15 @@ +title: Avocent ACS 800 CPU and Board Temperature +agents: snmp +catalog: hw/network/avocent +license: GPLv2 +distribution: check_mk +description: + Checks by SNMP the Temperature for CPU and Board sensors of Avocent ACS 800 devices. + + Return {OK} if no temperature rule is created, otherwise based on the level in + the configured temperature rule. +item: + CPU Temperature and Board Temperature + +discovery: + One service is created for CPU Temperature and one service for Board Temperature diff --git a/check_mk-avocent/cmk20-cmk21/share/check_mk/checkman/avocent_voltage_sensors b/check_mk-avocent/cmk20-cmk21/share/check_mk/checkman/avocent_voltage_sensors new file mode 100644 index 0000000..95b6744 --- /dev/null +++ b/check_mk-avocent/cmk20-cmk21/share/check_mk/checkman/avocent_voltage_sensors @@ -0,0 +1,15 @@ +title: Avocent ACS 800 Power Supply Voltage Sensors +agents: snmp +catalog: hw/network/avocent +license: GPLv2 +distribution: check_mk +description: + Checks by SNMP the power supply voltage sensors of Avocent ACS 800 devices. + + Returns {OK} Always. + +item: + The Voltage for each power supply. + +discovery: + Two services created, one for each Power Supply