Compare commits

...

37 Commits
2.0 ... master

Author SHA1 Message Date
d82a602c8d Update Hitachi VSP plugin for CheckMK 2.3+. 2025-06-03 13:59:42 +02:00
977fecdda0 Remove a couple stale files remaining from renames. 2025-05-26 17:39:26 +02:00
d7ac4ae68d cisco-ip-sla for CheckMK 2.3.0 looks like it was never committed. Here we go! 2025-05-26 17:39:26 +02:00
48000a39d7 Update cisco-ip-sla/local/share/check_mk/checks/cisco_ip_sla
matching also nxos
2025-05-26 17:39:26 +02:00
bbac7b11ca SCO-241: Update domain_checks plugin to use 2.3.0 CheckMK plugin API (since 2.4.0 requires this API). Also merge discovery and check GUIs into single pages. 2025-05-26 17:39:26 +02:00
475fde79c1 Rename directories to something more consistent, and less annoying when autocompleting on the CLI. 2025-05-26 17:39:26 +02:00
0b42f6485f Add updated mkp file for domain_checks. 2025-05-26 17:39:26 +02:00
aa8e8493a4 Add two extra includes. For some reason this plugin causes GUI exceptions in prod, even though none of the other plugin GUIs do. 2025-05-26 17:39:26 +02:00
509715cd38 Add plugin that performs various checks on domain names. 2025-05-26 17:39:26 +02:00
a4169fb638 Add CUCM plugin, since it was lying around in another repo. 2025-05-26 17:39:26 +02:00
1b4fafb15e Fix a bug where the summary for Defender alerts are entirely State.OK; the "details" field doesn't like an empty string. 2024-12-06 15:59:21 +01:00
6d39ee351b Switch to using single-row summaries for all the Defender alerts. Move the extra into into the details section. 2024-12-06 13:05:14 +01:00
1dca9e4a65 Switch to using resource groups with dynamic hosts. 2024-12-06 10:44:34 +01:00
87b27b3b49 Add support for configurable Azure Defender severity, plus fix a sporadic resource-group bug. 2024-11-26 17:59:34 +01:00
72e13f4204 Sometimes Azure returns no data for KeyVault latency (e.g. there were no KeyVault requests). Client wanted this to default to a latency of 0. 2024-11-19 18:29:16 +01:00
1b54b70a8d Fix Host header problem with proxy, switch Azure auth to type Fides uses. 2024-11-15 16:40:02 +01:00
d4b0c9497d Add support for proxy to Azure plugin. 2024-11-07 15:54:45 +01:00
270d9ac22c Rename some Azure plugin files just to be safe that they don't unexpectedly conflict with CheckMK's own built-in azure files. 2024-08-26 22:11:26 +02:00
36a998cfc6 Fix crit/warn bug with Defender alerts. 2024-08-25 23:20:00 +02:00
5fa472f450 Add initial plugin for monitoring Azure KeyVault and Firewall metrics, and Defender alerts. 2024-08-25 23:00:54 +02:00
52f417215b Forgot to add old Azure keyvault certificate checks. 2024-08-24 20:40:29 +02:00
62518edf2c Update AMD GPU plugin to work around item_name bug in CheckMK. 2024-07-25 12:38:09 +02:00
5ba99c1c15 Add AMD GPU plugin. 2024-06-17 17:27:56 +02:00
8f9970a40c Add some Sentry PDU changes that have been laying around for a few months. 2024-06-15 15:02:33 +02:00
775f9515a1 Add Sentry PDU (3rd line) outlet power checks, for CheckMK 1.6 and 2.2. 2024-02-27 08:47:28 +01:00
15f69ddc0c Update Graylog Input Metrics MKP due to download_url metadata. 2024-02-08 14:22:54 +01:00
3d9e54f470 Add Graylog Input Metrics plugin for CheckMK 2.2.0. 2024-02-08 14:16:13 +01:00
6de602a399 add selinux 2024-01-26 12:14:20 +02:00
44aa214b85 Add 'local checks/livestatusstats.py'
monitor some livestsatus stats, not sure it is completely right ..
2023-09-16 06:53:17 +00:00
bb424f90fe Add three Cisco plugins (GDOI, SLA, BGP) used for BCCD-13. 2023-07-03 15:14:25 +02:00
George Pochiscan
0aa461839b added checkmk checks for clever_pdu with firmware 1.2.0 and 1.3.0 2023-06-27 18:37:41 +03:00
George Pochiscan
d7ba6d93f4 added data checks 2023-05-31 12:47:38 +03:00
George Pochiscan
c26de603bb added sane check for line lenght 2023-05-26 12:24:19 +03:00
George Pochiscan
b3a43a96b1 added scality ring developed check 2023-02-24 12:45:49 +02:00
George Pochiscan
0f60e5725e added mk_informix extensions 2022-11-23 19:03:53 +02:00
a1b017d260 added 101 vers with 1.6p28 minver 2022-08-30 14:49:33 +03:00
George Pochiscan
2b522d2553 Added checks for Avocent ACS800 Devices for checkmk 2.0 and 2.1 2022-07-19 11:42:36 +03:00
268 changed files with 13370 additions and 0 deletions

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "juniper"]
path = juniper
url = https://github.com/spearheadsys/check_mk.git
[submodule "check_mk-check-selinux"]
path = selinux
url = https://code.spearhead.cloud/Spearhead/check_mk-check-selinux.git

BIN
amd-gpu/amd-gpu-0.1.1.mkp Executable file

Binary file not shown.

View File

@ -0,0 +1,106 @@
#!/usr/bin/env python3
#
# Copyright 2024 Spearhead Systems SRL
from cmk.base.plugins.agent_based.agent_based_api.v1 import (
register,
Service,
Result,
Metric,
State,
)
def discovery_amd_gpu(section):
name = section[0][0]
yield Service(item=name)
def check_state(alert_percentages, measured_percent):
if alert_percentages:
if alert_percentages[1] <= measured_percent:
return State.CRIT
elif alert_percentages[0] <= measured_percent:
return State.WARN
return State.OK
def get_levels(alert_levels, total=None):
if alert_levels == None:
return
if total == None:
return alert_levels
return (alert_levels[0] / 100 * total, alert_levels[1] / 100 * total)
def check_amd_gpu(item, params, section):
if item != section[0][0]:
return
gpu_percent = int(float(section[1][0]))
vram_bytes_used = int(section[2][0])
vram_bytes_total = int(section[3][0])
vram_bytes_free = max(0, vram_bytes_total - vram_bytes_used)
vram_mb_used = vram_bytes_used // 1048576
vram_mb_total = vram_bytes_total // 1048576
vram_mb_free = vram_bytes_free // 1048576
alert_gpu_percent = params.get("gpu_percent")
alert_vram_used_percent = params.get("vram_used_percent")
alert_vram_free_percent = params.get("vram_free_percent")
vram_used_percent = vram_bytes_used / vram_bytes_total * 100
vram_free_percent = 100 - vram_used_percent
yield Result(
state=check_state(alert_gpu_percent, gpu_percent),
summary=f"GPU: {gpu_percent}%"
)
yield Result(
state=check_state(alert_vram_free_percent, vram_free_percent),
summary=f"VRAM free: {vram_mb_free} MiB"
)
yield Result(
state=check_state(alert_vram_used_percent, vram_used_percent),
summary=f"VRAM used: {vram_mb_used} MiB"
)
yield Result(
state=State.OK,
summary=f"VRAM total: {vram_mb_total} MiB"
)
yield Metric(
name="gpu_percent",
value=gpu_percent,
levels=get_levels(alert_gpu_percent),
boundaries=(0, 100)
)
yield Metric(
name="vram_used",
value=vram_mb_used,
levels=get_levels(alert_vram_used_percent, vram_mb_total),
boundaries=(0, vram_mb_total)
)
yield Metric(
name="vram_free",
value=vram_mb_free,
levels=get_levels(alert_vram_free_percent, vram_mb_total),
boundaries=(0, vram_mb_total)
)
register.check_plugin(
name='amd_gpu',
service_name='AMD GPU - %s',
discovery_function=discovery_amd_gpu,
check_function=check_amd_gpu,
check_default_parameters={},
check_ruleset_name='amd_gpu',
)

View File

@ -0,0 +1,86 @@
#!/usr/bin/env python3
#
# Copyright 2024 Spearhead Systems SRL
from cmk.gui.i18n import _
from cmk.gui.plugins.wato.utils import (
CheckParameterRulespecWithItem,
rulespec_registry,
RulespecGroupCheckParametersHardware,
)
from cmk.gui.valuespec import Dictionary, Percentage, TextInput, Tuple
def _parameter_valuespec_amd_gpu():
return Dictionary(
title=_("GPU utilization"),
help=_(
"These metrics are queried directly from the AMD GPU. "
"Upper and lower levels can be specified for individual metrics."
),
elements=[
(
"gpu_percent",
Tuple(
title=_("GPU Used"),
help=_("If usage of total GPU compute goes above these percentages, issue alerts."),
elements=[
Percentage(
title=_("Warn if above"),
default_value=90
),
Percentage(
title=_("Crit if above"),
default_value=100
)
]
)
),
(
"vram_free_percent",
Tuple(
title=_("VRAM Free"),
help=_("If free VRAM goes above these percentages, issue alerts."),
elements=[
Percentage(
title="Warn if above",
default_value=70
),
Percentage(
title="Crit if above",
default_value=90
)
]
)
),
(
"vram_used_percent",
Tuple(
title=_("VRAM Used"),
help=_("If used VRAM goes above these percentages, issue alerts."),
elements=[
Percentage(
title="Warn if above",
default_value=70
),
Percentage(
title="Crit if above",
default_value=90
)
]
)
)
]
)
rulespec_registry.register(
CheckParameterRulespecWithItem(
check_group_name="amd_gpu",
group=RulespecGroupCheckParametersHardware,
match_type="dict",
parameter_valuespec=_parameter_valuespec_amd_gpu,
item_spec=lambda: TextInput(title=_("GPU")),
title=lambda: _("AMD GPU Metrics"),
)
)

View File

@ -0,0 +1,20 @@
# Copyright 2024 Spearhead Systems SRL
#
# This goes in C:\ProgramData\checkmk\agent\plugins. It should be added automatically by
# baking a new MSI after setting "Agent Rules" > "Deploy Custom Files With Agent" with
# "Deploy Custom Files With Agent" including "amd_gpu".
foreach ($Item in Get-ChildItem "HKLM:\SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}" -Name -Include 000*) {
$Name = Get-ItemPropertyValue "HKLM:\SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\$Item" "DriverDesc"
if ($Name -match 'Radeon') {
$GpuBytesTotal = Get-ItemPropertyValue "HKLM:\SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\$Item" "HardwareInformation.qwMemorySize"
$GpuRawName = Get-ItemPropertyValue "HKLM:\SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\$Item" "HardwareInformation.AdapterString"
break
}
}
$GpuName = [System.Text.Encoding]::Unicode.GetString($GpuRawName)
$GpuPercent = (((Get-Counter "\GPU Engine(*)\Utilization Percentage" ).CounterSamples).CookedValue | measure -sum).sum
$GpuBytesUsed = (((Get-Counter "\GPU Process Memory(*)\Dedicated Usage").CounterSamples).CookedValue | measure -sum).sum
Write-Output "<<<amd_gpu:sep(0)>>>", $GpuName, $GpuPercent, $GpuBytesUsed, $GpuBytesTotal

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,92 @@
#!/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 typing import List
from cmk.base.plugins.agent_based.agent_based_api.v1 import (
register,
Result,
Service,
SNMPTree,
startswith,
State,
)
from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import (
CheckResult,
DiscoveryResult,
StringTable,
)
Section = []
def parse_avocent_psu(string_table: List[StringTable]) -> 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
)

View File

@ -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"},
)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,72 @@
#!/usr/bin/env python3
# Copyright (C) 2023 Spearhead Systems SRL - License: GNU General Public License v2
import json
from datetime import datetime, timezone
from cmk.base.plugins.agent_based.agent_based_api.v1 import register, Result, Service, State
# Convert JSON entries into dictionaries indexed by certificate name.
def parse_keyvault(string_table):
raw_json = ""
cert_data = []
for row in string_table:
line = row[0]
raw_json += line
if line == "]":
cert_data.extend(json.loads(raw_json))
raw_json = ""
lookup = {}
for cert in cert_data:
lookup[cert["name"]] = cert
return lookup
register.agent_section(
name="azure_keyvault",
parse_function=parse_keyvault
)
# Produce a list of certificates based on the parsed output.
def discover_keyvault(section):
for name, details in sorted(section.items()):
yield Service(item=name)
# Given a specific certificate, look it up in the parsed output, and produce
# results on that service based upon the certificate's expiry.
def check_keyvault(item, params, section):
warn_days = params.get("warn_days")
crit_days = params.get("crit_days")
cert = section.get(item)
if cert is None:
return
expires = datetime.fromisoformat(cert["attributes"]["expires"])
now = datetime.now(timezone.utc)
remaining_days = (expires - now).days
state = State.OK
if crit_days is not None and remaining_days < crit_days:
state = State.CRIT
elif warn_days is not None and remaining_days < warn_days:
state = State.WARN
yield Result(state=state, summary="Expires in %d days" % remaining_days)
register.check_plugin(
name="azure_keyvault",
service_name="Azure Keyvault Certificate %s",
check_function=check_keyvault,
check_default_parameters={},
check_ruleset_name="azure_keyvault",
discovery_function=discover_keyvault,
)

View File

@ -0,0 +1,26 @@
#!/bin/bash
# Copyright (C) 2023 Spearhead Systems SRL - License: GNU General Public License v2
az=/usr/bin/az
set -euo pipefail
if [ "$#" -lt 4 ]; then
echo "Usage: $0 <tenant> <user> <password> <vault1> ... [vaultN]" >&2
exit 1
fi
tenant="$1"
user="$2"
password="$3"
vaults="${@:4}"
echo "<<<azure_keyvault:sep(0)>>>"
"$az" login --service-principal --tenant="$tenant" --user="$user" --password="$password" > /dev/null
for vault in $vaults; do
"$az" keyvault certificate list --vault-name="$vault"
done
"$az" logout

View File

@ -0,0 +1,16 @@
#!/usr/bin/env python3
# Copyright (C) 2023 Spearhead Systems SRL - License: GNU General Public License v2
def agent_azure_keyvault(params, hostname, ipaddress):
tenant = params["tenant"]
client = params["client"]
secret = params["secret"]
args = [tenant, client, secret]
for vault in params["vaults"]:
args.extend([vault.strip()])
return args
special_agent_info["azure_keyvault"] = agent_azure_keyvault

View File

@ -0,0 +1,110 @@
#!/usr/bin/env python3
# Copyright (C) 2023 Spearhead Systems SRL - License: GNU General Public License v2
import copy
from cmk.gui.i18n import _
from cmk.gui.plugins.wato.utils import (
rulespec_registry,
HostRulespec,
IndividualOrStoredPassword,
RulespecGroupCheckParametersDiscovery,
CheckParameterRulespecWithItem,
RulespecGroupCheckParametersApplications,
)
from cmk.gui.watolib.rulespecs import Rulespec
from cmk.gui.valuespec import (
Dictionary,
TextInput,
Integer,
ListOfStrings,
Password
)
def _valuespec_special_agents_azure_keyvault_check():
return Dictionary(
title=_("Azure Key Vault Certificate Checks"),
optional_keys=["warn_days", "crit_days"],
elements=[
(
"warn_days",
Integer(
minvalue=0,
default_value=30,
title=_("Certificate Days to Warn"),
help=_(
"How many days to warn before a certificate in this key vault will expire"
),
),
),
(
"crit_days",
Integer(
minvalue=0,
default_value=3,
title=_("Certificate Days to Crit"),
help=_(
"How many days to crit before a certificate in this key vault will expire"
),
),
),
],
)
def _valuespec_special_agents_azure_keyvault_discovery():
return Dictionary(
title=_("Azure Key Vault Certificate Discovery"),
elements=[
(
"tenant",
TextInput(
title=_("Tenant ID / Directory ID"),
allow_empty=False,
size=45,
),
),
(
"client",
TextInput(
title=_("Client ID / Application ID"),
allow_empty=False,
size=45,
),
),
(
"secret",
IndividualOrStoredPassword(
# Password(
title=_("Client Secret"),
allow_empty=False,
size=45,
),
),
(
"vaults",
ListOfStrings(
title=_("Keyvaults"),
allow_empty=False,
),
),
],
)
rulespec_registry.register(
CheckParameterRulespecWithItem(
check_group_name="azure_keyvault",
group=RulespecGroupCheckParametersApplications,
match_type='dict',
parameter_valuespec=_valuespec_special_agents_azure_keyvault_check,
)
)
rulespec_registry.register(
HostRulespec(
group=RulespecGroupCheckParametersDiscovery,
match_type='dict',
name="special_agents:azure_keyvault",
valuespec=_valuespec_special_agents_azure_keyvault_discovery,
)
)

BIN
azure/azure-spearhead-0.5.1.mkp Executable file

Binary file not shown.

View File

@ -0,0 +1,311 @@
#!/usr/bin/env python3
# Copyright (C) 2024 Spearhead Systems SRL
import json
from datetime import datetime, timezone
from cmk.base.plugins.agent_based.agent_based_api.v1 import register, Result, Service, State, Metric
def check_state_below(alert_percentages, measured_percent):
if alert_percentages:
if alert_percentages[1] >= measured_percent:
return State.CRIT
elif alert_percentages[0] >= measured_percent:
return State.WARN
return State.OK
def check_state_above(alert_percentages, measured_percent):
if alert_percentages:
if alert_percentages[1] <= measured_percent:
return State.CRIT
elif alert_percentages[0] <= measured_percent:
return State.WARN
return State.OK
# Convert JSON entries into dictionaries indexed by name. We're assuming here
# that the name is unique across AZs. If not, add the 'location' field in each
# object to the name.
def parse(string_table):
lookup = {}
for json_data in string_table:
obj = json.loads(json_data[0])
name = obj["name"]
group = obj["resource_group"]
lookup[f"{name}#{group}"] = obj
return lookup
# Produce a list of Azure objects for discovery. This applies for KeyVault and
# Firewall.
def discover(section):
for name, details in sorted(section.items()):
yield Service(item=name)
# Produce a list of Azure resource group objects for discovery. This applies to
# Defender. We also assume each section comes entirely from the same resource
# group (this should be true given our special agent).
def discover_defender(section):
items = list(section.values())
if items != []:
yield Service(item=items[0]["resource_group"])
else:
yield Service(item=None)
# Given a specific keyvault metric, look it up in the parsed output, and produce
# results on that service based upon the metric's range.
def check_keyvault(item, params, section):
vault = section.get(item)
if vault is None:
return
metrics = vault["metrics"]
availability = metrics.get("Availability")
capacity = metrics.get("SaturationShoebox")
latency = metrics.get("ServiceApiLatency", 0)
hits = metrics.get("ServiceApiHit")
results = metrics.get("ServiceApiResult")
alert_availability_percent = params.get("availability")
alert_capacity_percent = params.get("capacity")
alert_latency_milliseconds = params.get("latency")
if availability is not None:
yield Result(
state=check_state_below(alert_availability_percent, availability),
summary=f"Availability: {availability}%",
)
yield Metric(
name="availability",
value=availability,
boundaries=(0, 100),
)
else:
yield Result(
state=State.UNKNOWN,
summary="Availability: N/A",
)
if capacity is not None:
yield Result(
state=check_state_above(alert_capacity_percent, capacity),
summary=f"Capacity: {capacity}%"
)
yield Metric(
name="capacity",
value=capacity,
boundaries=(0, 100),
)
else:
yield Result(
state=State.UNKNOWN,
summary="Capacity: N/A",
)
if latency is not None:
yield Result(
state=check_state_above(alert_latency_milliseconds, latency),
summary=f"Latency: {latency}ms",
)
yield Metric(
name="latency",
value=latency,
boundaries=(0, None),
)
else:
yield Result(
state=State.UNKNOWN,
summary="Latency: N/A",
)
if hits is not None:
yield Metric(
name="hits",
value=hits,
boundaries=(0, None),
)
else:
yield Result(
state=State.UNKNOWN,
summary="Hits: N/A",
)
if results is not None:
yield Metric(
name="results",
value=results,
boundaries=(0, None),
)
else:
yield Result(
state=State.UNKNOWN,
summary="Results: N/A",
)
# Given a specific firewall metric, look it up in the parsed output, and produce
# results on that service based upon the metric's range.
def check_firewall(item, params, section):
firewall = section.get(item)
if firewall is None:
return
metrics = firewall["metrics"]
availability = metrics.get("FirewallHealth")
throughput = metrics.get("Throughput")
latency = metrics.get("FirewallLatencyPng")
alert_availability_percent = params.get("availability")
alert_latency_milliseconds = params.get("latency")
if availability is not None:
yield Result(
state=check_state_below(alert_availability_percent, availability),
summary=f"Availability: {availability}%",
)
yield Metric(
name="availability",
value=availability,
boundaries=(0, 100)
)
else:
yield Result(
state=State.UNKNOWN,
summary="Availability: N/A",
)
if latency is not None:
yield Result(
state=check_state_above(alert_latency_milliseconds, latency),
summary=f"Latency: {latency}ms",
)
yield Metric(
name="latency",
value=latency,
boundaries=(0, None)
)
else:
yield Result(
state=State.UNKNOWN,
summary="Latency: N/A",
)
if throughput is not None:
yield Metric(
name="throughput",
value=throughput,
boundaries=(0, None)
)
else:
yield Result(
state=State.UNKNOWN,
summary="Throughput: N/A",
)
def check_defender(item, params, section):
num_high = 0
num_med = 0
num_low = 0
num_info = 0
final_state = State.OK
region_details = []
for name, alert in sorted(section.items()):
details = alert["alert"]
status = details["status"]
if status != "Active" and status != "InProgress":
continue
severity = details["severity"]
url = details["url"]
info = details["info"]
if severity == "High":
num_high += 1
state = State(params.get("severity_high", State.CRIT))
elif severity == "Medium":
num_med += 1
state = State(params.get("severity_medium", State.WARN))
elif severity == "Low":
num_low += 1
state = State(params.get("severity_low", State.WARN))
elif severity == "Informational":
num_info += 1
state = State(params.get("severity_informational", State.OK))
else:
state = State.UNKNOWN
final_state = State(max(final_state.value, state.value))
if state.value > State.OK.value:
region_details.append(f"{severity}: {info}: {url}")
if region_details == []:
region_details = ["No Defender alerts"]
yield Result(
state=final_state,
summary=f"High: {num_high}, Medium: {num_med}, Low: {num_low}, Informational: {num_info}",
details="\n".join(region_details)
)
register.agent_section(
name="azure_keyvault",
parse_function=parse
)
register.check_plugin(
name="azure_keyvault",
service_name="Azure Keyvault Metric %s",
check_function=check_keyvault,
check_default_parameters={},
check_ruleset_name="azure_keyvault",
discovery_function=discover,
)
register.agent_section(
name="azure_firewall",
parse_function=parse
)
register.check_plugin(
name="azure_firewall",
service_name="Azure Firewall Metric %s",
check_function=check_firewall,
check_default_parameters={},
check_ruleset_name="azure_firewall",
discovery_function=discover,
)
register.agent_section(
name="azure_defender",
parse_function=parse
)
register.check_plugin(
name="azure_defender",
service_name="Azure Defender Alert %s",
check_function=check_defender,
check_default_parameters={},
check_ruleset_name="azure_defender",
discovery_function=discover_defender,
)

View File

@ -0,0 +1,215 @@
#!/usr/bin/env python3
# Copyright (C) 2024 Spearhead Systems SRL
from urllib import request, parse, error
from datetime import datetime, timezone, timedelta
import json
import sys
import re
VAULT_METRICS = [
'Availability',
'SaturationShoebox',
'ServiceApiLatency',
'ServiceApiHit',
'ServiceApiResult',
]
FIREWALL_METRICS = [
'FirewallHealth',
'Throughput',
'FirewallLatencyPng',
]
REGION_RE = re.compile('/locations/(.+?)/')
RESOURCE_GROUP_RE = re.compile('/resourceGroups/(.+?)/')
# https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/request-limits-and-throttling
def get_url(req, default):
#import http.client
#http.client.HTTPConnection.debuglevel = 1
try:
res = request.urlopen(req)
return res.read()
except error.HTTPError as e:
if e.code == 429:
return default
else:
raise e
def set_proxy(req, proxy):
if proxy is None or proxy == '':
return
match = re.match('(https?)://(.+?)/?$', proxy, re.I)
req.set_proxy(match[2], match[1].lower())
# The explicit Host header is required for this to also work with a proxy.
# If we don't include it, Python sends the proxy's Host to Microsoft
# instead! So we have to set the Host to the Microsoft domain manually.
match = re.match('https://(.+?)/', req.full_url, re.I)
req.add_header('Host', match[1] + ":443")
def get_token(tenant, username, password, proxy):
data = parse.urlencode({
'client_id': username,
'client_secret': password,
'grant_type': 'client_credentials',
'claims': '{"access_token": {"xms_cc": {"values": ["CP1"]}}}',
'scope': 'https://management.core.windows.net//.default offline_access openid profile',
'client_info': 1,
})
req = request.Request(f'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token',
data=str.encode(data))
set_proxy(req, proxy)
res = get_url(req, None)
if res is None:
return
token_data = json.loads(res)
token = token_data['access_token']
return token
def get_json(token, proxy, path, version='2023-07-01'):
url = f"https://management.azure.com{path}{'?' in path and '&' or '?'}api-version={version}"
req = request.Request(url, headers={'Authorization': f'Bearer {token}'})
set_proxy(req, proxy)
res = get_url(req, "[]")
data = json.loads(res)
return data['value']
def list_subscriptions(token, proxy):
return get_json(token, proxy, '/subscriptions')
def list_vaults(token, proxy, subscription):
return get_json(token, proxy, f'/subscriptions/{subscription}/resources?$filter=resourceType%20eq%20%27Microsoft.KeyVault%2Fvaults%27')
def list_firewalls(token, proxy, subscription):
return get_json(token, proxy, f'/subscriptions/{subscription}/resources?$filter=resourceType%20eq%20%27Microsoft.Network%2FazureFirewalls%27')
def list_defender_alerts(token, proxy, subscription):
return get_json(token, proxy, f'/subscriptions/{subscription}/providers/Microsoft.Security/alerts', '2022-01-01')
def get_recent_metrics(token, proxy, path, metrics):
end = datetime.now()
start = end - timedelta(minutes=2)
start_str = start.isoformat().split('.')[0] + 'Z'
end_str = end.isoformat().split('.')[0] + 'Z'
metrics_str = ','.join(metrics)
return get_json(token, proxy, f'{path}/providers/microsoft.insights/metrics?metricnames={metrics_str}&timespan={start_str}/{end_str}', '2023-10-01')
def metrics_to_lookup(metrics):
lookup = {}
for metric in metrics:
name = metric['name']['value']
series = metric['timeseries']
if series:
value = series[0]['data'][-1]
key = next(filter(lambda foo: foo != 'timeStamp', value), None)
lookup[name] = value.get(key)
return lookup
def get_args(argv):
if (len(argv) != 5 and len(argv) != 6) or argv[1] not in ['keyvault', 'firewall', 'defender']:
print(f"{sys.argv[0]} <command> <tenant ID> <username> <password> <proxy>", file=sys.stderr)
print(f"Valid commands are: 'keyvault', 'firewall', 'defender'", file=sys.stderr)
print(f"Proxy is an optional argument", file=sys.stderr)
exit(1)
return argv[1], argv[2], argv[3], argv[4], (argv[5] if len(argv) == 6 else None)
def print_json(obj):
print(json.dumps(obj))
def get_resource_group(obj):
found = re.search(RESOURCE_GROUP_RE, obj['id'])
if found:
return found[1]
return None
resource_groups = {}
command, tenant, username, password, proxy = get_args(sys.argv)
token = get_token(tenant, username, password, proxy)
for subscription in list_subscriptions(token, proxy):
subscription_id = subscription['subscriptionId']
if command == 'defender':
for alert in list_defender_alerts(token, proxy, subscription_id):
properties = alert['properties']
status = properties['status']
if not status in ['Active', 'InProgress']:
continue
group = get_resource_group(alert)
resource_groups.setdefault(group, []).append({
'type': command,
'name': alert['name'],
'location': re.search(REGION_RE, alert['id'])[1],
'resource_group': group,
'alert': {
'status': status,
'severity': properties['severity'],
'url': properties['alertUri'],
'info': properties['alertDisplayName']
}
})
elif command == 'firewall':
for firewall in list_firewalls(token, proxy, subscription_id):
metrics = get_recent_metrics(token, proxy, firewall['id'], FIREWALL_METRICS)
group = get_resource_group(firewall)
resource_groups.setdefault(group, []).append({
'type': command,
'name': firewall['name'],
'location': firewall['location'],
'resource_group': get_resource_group(firewall),
'metrics': metrics_to_lookup(metrics),
})
elif command == 'keyvault':
for vault in list_vaults(token, proxy, subscription_id):
metrics = get_recent_metrics(token, proxy, vault['id'], VAULT_METRICS)
group = get_resource_group(vault)
resource_groups.setdefault(group, []).append({
'type': command,
'name': vault['name'],
'location': vault['location'],
'resource_group': group,
'metrics': metrics_to_lookup(metrics),
})
for group, results in resource_groups.items():
if group is None:
print(f"<<<<>>>>")
else:
print(f"<<<<{group}>>>>")
print(f"<<<azure_{command}:sep(0)>>>")
for result in results:
print_json(result)

View File

@ -0,0 +1,4 @@
#!/bin/bash
dir=$(dirname -- "${BASH_SOURCE[0]}")
"$dir"/agent_azure_common defender "$1" "$2" "$3" "$4"

View File

@ -0,0 +1,4 @@
#!/bin/bash
dir=$(dirname -- "${BASH_SOURCE[0]}")
"$dir"/agent_azure_common firewall "$1" "$2" "$3" "$4"

View File

@ -0,0 +1,4 @@
#!/bin/bash
dir=$(dirname -- "${BASH_SOURCE[0]}")
"$dir"/agent_azure_common keyvault "$1" "$2" "$3" "$4"

View File

@ -0,0 +1,25 @@
#!/usr/bin/env python3
# Copyright (C) 2024 Spearhead Systems SRL
import cmk.utils.password_store
def agent_azure_args(params, hostname, ipaddress):
# Extract password either from params, or from password store:
# ('password', '<some password>'): password is in params directly
# ('store', '<password name>'): password must be looked up in store by name
password_info = params["password"]
if password_info[0] == "password":
password = password_info[1]
else:
password = cmk.utils.password_store.extract(password_info[1])
return [
params["tenant"],
params["username"],
password,
params.get("proxy") or "" # optional
]
special_agent_info["azure_keyvault"] = agent_azure_args
special_agent_info["azure_firewall"] = agent_azure_args
special_agent_info["azure_defender"] = agent_azure_args

View File

@ -0,0 +1,286 @@
#!/usr/bin/env python3
# Copyright (C) 2024 Spearhead Systems SRL
import copy
from cmk.base.plugins.agent_based.agent_based_api.v1 import State
from cmk.gui.i18n import _
from cmk.gui.plugins.wato.utils import (
rulespec_registry,
HostRulespec,
IndividualOrStoredPassword,
RulespecGroupCheckParametersDiscovery,
CheckParameterRulespecWithItem,
RulespecGroupCheckParametersApplications,
)
from cmk.gui.watolib.rulespecs import Rulespec
from cmk.gui.valuespec import (
Dictionary,
TextInput,
Integer,
ListOfStrings,
Password
)
def _discovery(title):
return Dictionary(
title=_(title),
required_keys=["tenant", "username", "password"],
elements=[
(
"tenant",
TextInput(
title=_("Tenant ID / Directory ID"),
allow_empty=False,
),
),
(
"username",
TextInput(
title=_("Client ID / Application ID"),
allow_empty=False,
),
),
(
"password",
IndividualOrStoredPassword(
# Password(
title=_("Client Secret"),
allow_empty=False,
),
),
(
"proxy",
TextInput(
title=_("Proxy"),
),
),
],
)
def _valuespec_special_agents_azure_keyvault_discovery():
return _discovery("Azure Key Vault Metrics Discovery")
def _valuespec_special_agents_azure_firewall_discovery():
return _discovery("Azure Firewall Metrics Discovery")
def _valuespec_special_agents_azure_defender_discovery():
return _discovery("Azure Defender Alerts Discovery")
def _valuespec_special_agents_azure_keyvault_check():
return Dictionary(
title=_("Azure Key Vault Metric Checks"),
elements=[
(
"availability",
Tuple(
title=_("Availability"),
help=_("If drops below these percentages over the past minute, issue alert"),
elements=[
Percentage(
title=_("Warn if below"),
default_value=98
),
Percentage(
title=_("Crit if below"),
default_value=90
)
]
)
),
(
"capacity",
Tuple(
title=_("Capacity used"),
help=_("If goes above these percentages over the past minute, issue alert"),
elements=[
Percentage(
title=_("Warn if above"),
default_value=80
),
Percentage(
title=_("Crit if above"),
default_value=98
)
]
)
),
(
"latency",
Tuple(
title=_("Request latency"),
help=_("If goes above the average milliseconds over the past minute, issue alert"),
elements=[
Integer(
title=_("Warn if above"),
default_value=100,
minvalue=0,
),
Integer(
title=_("Crit if above"),
default_value=2000,
minvalue=0,
)
]
)
),
],
)
def _valuespec_special_agents_azure_firewall_check():
return Dictionary(
title=_("Azure Firewall Metric Checks"),
elements=[
(
"availability",
Tuple(
title=_("Availability"),
help=_("If drops below these percentages over the past minute, issue alert"),
elements=[
Percentage(
title=_("Warn if below"),
default_value=98
),
Percentage(
title=_("Crit if below"),
default_value=90
)
]
)
),
(
"latency",
Tuple(
title=_("Request latency"),
help=_("If goes above the average milliseconds over the past minute, issue alert"),
elements=[
Integer(
title=_("Warn if above"),
default_value=100,
minvalue=0,
),
Integer(
title=_("Crit if above"),
default_value=2000,
minvalue=0,
)
]
)
),
],
)
def _valuespec_special_agents_azure_defender_check():
return Dictionary(
title=_("Azure Defender Alerts Severity"),
elements=[
(
"severity_high",
DropdownChoice(
title=_("Defender severity 'High'"),
help=_("What CheckMK criticality should this Azure Defender severity trigger"),
default_value=State.CRIT.value,
choices=[
(State.CRIT.value, _(State.CRIT.name)),
(State.WARN.value, _(State.WARN.name)),
(State.OK.value, _(State.OK.name)),
],
),
),
(
"severity_medium",
DropdownChoice(
title=_("Defender severity 'Medium'"),
help=_("What CheckMK criticality should this Azure Defender severity trigger"),
default_value=State.WARN.value,
choices=[
(State.CRIT.value, _(State.CRIT.name)),
(State.WARN.value, _(State.WARN.name)),
(State.OK.value, _(State.OK.name)),
],
),
),
(
"severity_low",
DropdownChoice(
title=_("Defender severity 'Low'"),
help=_("What CheckMK criticality should this Azure Defender severity trigger"),
default_value=State.WARN.value,
choices=[
(State.CRIT.value, _(State.CRIT.name)),
(State.WARN.value, _(State.WARN.name)),
(State.OK.value, _(State.OK.name)),
],
),
),
(
"severity_informational",
DropdownChoice(
title=_("Defender severity 'Informational'"),
help=_("What CheckMK criticality should this Azure Defender severity trigger"),
default_value=State.OK.value,
choices=[
(State.CRIT.value, _(State.CRIT.name)),
(State.WARN.value, _(State.WARN.name)),
(State.OK.value, _(State.OK.name)),
],
),
),
],
)
rulespec_registry.register(
HostRulespec(
name="special_agents:azure_keyvault",
group=RulespecGroupCheckParametersDiscovery,
match_type='dict',
valuespec=_valuespec_special_agents_azure_keyvault_discovery,
)
)
rulespec_registry.register(
HostRulespec(
name="special_agents:azure_firewall",
group=RulespecGroupCheckParametersDiscovery,
match_type='dict',
valuespec=_valuespec_special_agents_azure_firewall_discovery,
)
)
rulespec_registry.register(
HostRulespec(
name="special_agents:azure_defender",
group=RulespecGroupCheckParametersDiscovery,
match_type='dict',
valuespec=_valuespec_special_agents_azure_defender_discovery,
)
)
rulespec_registry.register(
CheckParameterRulespecWithItem(
check_group_name="azure_keyvault",
group=RulespecGroupCheckParametersApplications,
match_type="dict",
parameter_valuespec=_valuespec_special_agents_azure_keyvault_check,
item_spec=lambda: TextInput(title=_("Key Vault")),
title=lambda: _("Azure Key Vault Metrics"),
)
)
rulespec_registry.register(
CheckParameterRulespecWithItem(
check_group_name="azure_firewall",
group=RulespecGroupCheckParametersApplications,
match_type="dict",
parameter_valuespec=_valuespec_special_agents_azure_firewall_check,
item_spec=lambda: TextInput(title=_("Firewall")),
title=lambda: _("Azure Firewall Metrics"),
)
)
rulespec_registry.register(
CheckParameterRulespecWithItem(
check_group_name="azure_defender",
group=RulespecGroupCheckParametersApplications,
match_type="dict",
parameter_valuespec=_valuespec_special_agents_azure_defender_check,
item_spec=lambda: TextInput(title=_("Defender")),
title=lambda: _("Azure Defender Alerts Severity"),
)
)

Binary file not shown.

1
cisco-bgp-peer/README.md Normal file
View File

@ -0,0 +1 @@
This is a modification of a GPL plugin to additionally support CISCO-BGP4-MIB::CbgpPeer3Entry

View File

@ -0,0 +1,665 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# License: GNU General Public License v2
#
# Author: thl-cmk[at]outlook[dot]com
# URL : https://thl-cmk.hopto.org
# Date : 2017-12-26
#
# Monitor status of Cisco BGP Peer (IPv4 and IPv6)
#
# 2018-05-24: changed counters to 1/s
# 2018-05-25: a lot of code cleanup
# packet name changed from cisco_bgp to cisco_bgp_peer
# added support of more then one address family per peer
# (changed item from remoteip to remoteip+familyname, rewrite of parer, inventory and check function)
# 2018-05-27: changed scan function from '.1.3.6.1.4.1.9.9.187.1.2.7.1.3.* to sysdecr contains cisco
# 2018-05-28: changed wato, added peer alias, state if not found, infotext values
# 2018-05-29: fixed longoutpout (removed not configured)
# 2018-11-02: modified scanfunction (from "find 'cisco' =-1" to "'cisco' in OID"
# 2019-18-02: added fix for empty values ("" instead of "0") sugested by Laurent Barbier (lbarbier[at]arkane-studios[dot]com)
# 2020-02-24: added workaround for missing cbgpPeer2AddrFamily (example L2VPN EVPN peers, Fix for jonale82[at]gmail[dot]com)
# 2020-03-02: changed handling of perfdata, add only data the are really there (not None, instead of setting them to 0)
# 2020-06-04: code cleanup --> changed isdigit test to try/except loop, changed peer.get test to try/except loop
# 2020-09-10: fixed typo in metrics file. FMS --> FSM (thanks martin[dot]pechstein[at]posteo[dot]de)
# 2021-03-27: rewrite for CMK2.0
# 2021-03-28: added warning for missing admin prefix limit/warn threshold
# 2023-01-15: modified by Spearhead Systems to support CISCO-BGP4-MIB::CbgpPeer3Entry
#
# snmpwalk sample
#
# CISCO-BGP4-MIB::cbgpPeer2AddrFamilyEntry
#
# OMD[mysite]:~$ snmpwalk -ObentU -v2c -c <removed> simulant 1.3.6.1.4.1.9.9.187.1.2.7
# .1.3.6.1.4.1.9.9.187.1.2.7.1.3.1.4.62.214.127.57.1.1 = STRING: "IPv4 Unicast"
# .1.3.6.1.4.1.9.9.187.1.2.7.1.3.1.4.217.119.208.1.1.1 = STRING: "IPv4 Unicast"
# .1.3.6.1.4.1.9.9.187.1.2.7.1.3.1.4.217.119.208.33.1.1 = STRING: "IPv4 Unicast"
# .1.3.6.1.4.1.9.9.187.1.2.7.1.3.2.16.32.1.20.56.7.0.0.39.0.0.0.0.0.0.0.1.2.1 = STRING: "IPv6 Unicast"
# .1.3.6.1.4.1.9.9.187.1.2.7.1.3.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16.2.1 = STRING: "IPv6 Unicast"
#
# CISCO-BGP4-MIB::CbgpPeer2Entry (IPv4)
#
# OMD[mysite]:~$ snmpwalk -ObentU -v2c -c <removed> simulant 1.3.6.1.4.1.9.9.187.1.2.5.1| grep 62.214.127.57
# .1.3.6.1.4.1.9.9.187.1.2.5.1.3.1.4.62.214.127.57 = INTEGER: 6
# .1.3.6.1.4.1.9.9.187.1.2.5.1.4.1.4.62.214.127.57 = INTEGER: 2
# .1.3.6.1.4.1.9.9.187.1.2.5.1.5.1.4.62.214.127.57 = INTEGER: 4
# .1.3.6.1.4.1.9.9.187.1.2.5.1.6.1.4.62.214.127.57 = Hex-STRING: 3E D6 7F 3A
# .1.3.6.1.4.1.9.9.187.1.2.5.1.7.1.4.62.214.127.57 = Gauge32: 29418
# .1.3.6.1.4.1.9.9.187.1.2.5.1.8.1.4.62.214.127.57 = Gauge32: 0
# .1.3.6.1.4.1.9.9.187.1.2.5.1.9.1.4.62.214.127.57 = IpAddress: 217.119.208.2
# .1.3.6.1.4.1.9.9.187.1.2.5.1.10.1.4.62.214.127.57 = Gauge32: 179
# .1.3.6.1.4.1.9.9.187.1.2.5.1.11.1.4.62.214.127.57 = Gauge32: 8881
# .1.3.6.1.4.1.9.9.187.1.2.5.1.12.1.4.62.214.127.57 = IpAddress: 62.214.127.57
# .1.3.6.1.4.1.9.9.187.1.2.5.1.13.1.4.62.214.127.57 = Counter32: 18
# .1.3.6.1.4.1.9.9.187.1.2.5.1.14.1.4.62.214.127.57 = Counter32: 2
# .1.3.6.1.4.1.9.9.187.1.2.5.1.15.1.4.62.214.127.57 = Counter32: 205
# .1.3.6.1.4.1.9.9.187.1.2.5.1.16.1.4.62.214.127.57 = Counter32: 195
# .1.3.6.1.4.1.9.9.187.1.2.5.1.17.1.4.62.214.127.57 = Hex-STRING: 00 00
# .1.3.6.1.4.1.9.9.187.1.2.5.1.18.1.4.62.214.127.57 = Counter32: 1
# .1.3.6.1.4.1.9.9.187.1.2.5.1.19.1.4.62.214.127.57 = Gauge32: 10446
# .1.3.6.1.4.1.9.9.187.1.2.5.1.20.1.4.62.214.127.57 = INTEGER: 60
# .1.3.6.1.4.1.9.9.187.1.2.5.1.21.1.4.62.214.127.57 = INTEGER: 180
# .1.3.6.1.4.1.9.9.187.1.2.5.1.22.1.4.62.214.127.57 = INTEGER: 60
# .1.3.6.1.4.1.9.9.187.1.2.5.1.23.1.4.62.214.127.57 = INTEGER: 180
# .1.3.6.1.4.1.9.9.187.1.2.5.1.24.1.4.62.214.127.57 = INTEGER: 60
# .1.3.6.1.4.1.9.9.187.1.2.5.1.25.1.4.62.214.127.57 = INTEGER: 30
# .1.3.6.1.4.1.9.9.187.1.2.5.1.26.1.4.62.214.127.57 = INTEGER: 30
# .1.3.6.1.4.1.9.9.187.1.2.5.1.27.1.4.62.214.127.57 = Gauge32: 5824
# .1.3.6.1.4.1.9.9.187.1.2.5.1.28.1.4.62.214.127.57 = ""
# .1.3.6.1.4.1.9.9.187.1.2.5.1.29.1.4.62.214.127.57 = INTEGER: 5
#
#
# CISCO-BGP4-MIB::CbgpPeer2Entry (IPv6)
#
# OMD[mysite]:~$ snmpwalk -ObentU -v2c -c <removed> simulant 1.3.6.1.4.1.9.9.187.1.2.5.1| grep 16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16
# .1.3.6.1.4.1.9.9.187.1.2.5.1.3.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = INTEGER: 6
# .1.3.6.1.4.1.9.9.187.1.2.5.1.4.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = INTEGER: 2
# .1.3.6.1.4.1.9.9.187.1.2.5.1.5.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = INTEGER: 4
# .1.3.6.1.4.1.9.9.187.1.2.5.1.6.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = Hex-STRING: 2A 05 57 C0 00 00 FF FF 00 00 00 00 00 00 00 11
# .1.3.6.1.4.1.9.9.187.1.2.5.1.7.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = Gauge32: 179
# .1.3.6.1.4.1.9.9.187.1.2.5.1.8.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = Gauge32: 0
# .1.3.6.1.4.1.9.9.187.1.2.5.1.9.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = IpAddress: 217.119.208.2
# .1.3.6.1.4.1.9.9.187.1.2.5.1.10.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = Gauge32: 35062
# .1.3.6.1.4.1.9.9.187.1.2.5.1.11.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = Gauge32: 31259
# .1.3.6.1.4.1.9.9.187.1.2.5.1.12.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = IpAddress: 217.119.208.1
# .1.3.6.1.4.1.9.9.187.1.2.5.1.13.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = Counter32: 5
# .1.3.6.1.4.1.9.9.187.1.2.5.1.14.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = Counter32: 6
# .1.3.6.1.4.1.9.9.187.1.2.5.1.15.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = Counter32: 157
# .1.3.6.1.4.1.9.9.187.1.2.5.1.16.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = Counter32: 161
# .1.3.6.1.4.1.9.9.187.1.2.5.1.17.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = Hex-STRING: 06 04
# .1.3.6.1.4.1.9.9.187.1.2.5.1.18.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = Counter32: 2
# .1.3.6.1.4.1.9.9.187.1.2.5.1.19.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = Gauge32: 8430
# .1.3.6.1.4.1.9.9.187.1.2.5.1.20.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = INTEGER: 60
# .1.3.6.1.4.1.9.9.187.1.2.5.1.21.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = INTEGER: 180
# .1.3.6.1.4.1.9.9.187.1.2.5.1.22.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = INTEGER: 60
# .1.3.6.1.4.1.9.9.187.1.2.5.1.23.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = INTEGER: 180
# .1.3.6.1.4.1.9.9.187.1.2.5.1.24.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = INTEGER: 60
# .1.3.6.1.4.1.9.9.187.1.2.5.1.25.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = INTEGER: 0
# .1.3.6.1.4.1.9.9.187.1.2.5.1.26.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = INTEGER: 0
# .1.3.6.1.4.1.9.9.187.1.2.5.1.27.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = Gauge32: 1494
# .1.3.6.1.4.1.9.9.187.1.2.5.1.28.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = STRING: "Administrative Reset"
# .1.3.6.1.4.1.9.9.187.1.2.5.1.29.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16 = INTEGER: 5
#
#
# CISCO-BGP4-MIB::cbgpPeer2AddrFamilyPrefixEntry (IPv4)
#
# OMD[mysite]:~$ snmpwalk -ObentU -v2c -c <removed> simulant 1.3.6.1.4.1.9.9.187.1.2.8.1| grep 16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16
# .1.3.6.1.4.1.9.9.187.1.2.8.1.1.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16.2.1 = Counter32: 2
# .1.3.6.1.4.1.9.9.187.1.2.8.1.2.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16.2.1 = Gauge32: 0
# .1.3.6.1.4.1.9.9.187.1.2.8.1.3.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16.2.1 = Gauge32: 100000
# .1.3.6.1.4.1.9.9.187.1.2.8.1.4.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16.2.1 = Gauge32: 85
# .1.3.6.1.4.1.9.9.187.1.2.8.1.5.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16.2.1 = Gauge32: 80
# .1.3.6.1.4.1.9.9.187.1.2.8.1.6.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16.2.1 = Gauge32: 10
# .1.3.6.1.4.1.9.9.187.1.2.8.1.7.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16.2.1 = Gauge32: 0
# .1.3.6.1.4.1.9.9.187.1.2.8.1.8.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16.2.1 = Gauge32: 0
#
#
# CISCO-BGP4-MIB::cbgpPeer2AddrFamilyPrefixEntry (IPv6)
#
# OMD[mysite]:~$ snmpwalk -ObentU -v2c -c <removed> simulant 1.3.6.1.4.1.9.9.187.1.2.8.1| grep 16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16
# .1.3.6.1.4.1.9.9.187.1.2.8.1.1.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16.2.1 = Counter32: 2
# .1.3.6.1.4.1.9.9.187.1.2.8.1.2.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16.2.1 = Gauge32: 0
# .1.3.6.1.4.1.9.9.187.1.2.8.1.3.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16.2.1 = Gauge32: 100000
# .1.3.6.1.4.1.9.9.187.1.2.8.1.4.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16.2.1 = Gauge32: 85
# .1.3.6.1.4.1.9.9.187.1.2.8.1.5.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16.2.1 = Gauge32: 80
# .1.3.6.1.4.1.9.9.187.1.2.8.1.6.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16.2.1 = Gauge32: 10
# .1.3.6.1.4.1.9.9.187.1.2.8.1.7.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16.2.1 = Gauge32: 0
# .1.3.6.1.4.1.9.9.187.1.2.8.1.8.2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.16.2.1 = Gauge32: 0
#
#
# sample info
#
# [
# [
# ['1.4.77.235.182.229', '6', '2', 'M\xeb\xb6\xe6', '0', '217.119.208.1', '21413', '77.235.182.229', '1', '3', '48',
# '53', '\x04\x00', '8', '2581', '2581', 'hold time expired', '5'],
# ['1.4.217.119.208.2', '6', '2', '\xd9w\xd0\x01', '0', '217.119.208.1', '31259', '217.119.208.2', '11', '23', '168',
# '170', '\x06\x04', '3', '8380', '5774', 'Administrative Reset', '5'],
# ['1.4.217.119.208.34', '6', '2', '\xd9w\xd0!', '0', '217.119.208.1', '31259', '217.119.208.2', '11', '23', '168',
# '170', '\x06\x04', '3', '8377', '5774', 'Administrative Reset', '5'],
# ['2.16.42.0.28.160.16.0.1.53.0.0.0.0.0.0.0.1', '6', '2', '*\x00\x1c\xa0\x10\x00\x015\x00\x00\x00\x00\x00\x00\x00\x02',
# '0', '217.119.208.1', '21413', '77.235.182.229', '0', '4', '108', '121', '\x06\x04', '6', '6295', '0',
# 'Administrative Reset', '5'],
# ['2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.17', '6', '2', '*\x05W\xc0\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x10',
# '0', '217.119.208.1', '31259', '217.119.208.2', '6', '5', '160', '157', '\x06\x04', '2', '8380', '1409',
# 'Administrative Reset', '5']
# ],
# [
# ['1.4.77.235.182.229.1.1', 'IPv4 Unicast', '1', '0', '', '', '', '6', '0', '0'],
# ['1.4.217.119.208.2.1.1', 'IPv4 Unicast', '4', '0', '', '', '', '17', '0', '10'],
# ['1.4.217.119.208.34.1.1', 'IPv4 Unicast', '4', '0', '', '', '', '17', '0', '10'],
# ['2.16.42.0.28.160.16.0.1.53.0.0.0.0.0.0.0.1.2.1', 'IPv6 Unicast', '0', '0', '100000', '85', '80', '6', '0', '0'],
# ['2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.17.2.1', 'IPv6 Unicast', '2', '0', '100000', '85', '80', '8', '0', '0']
# ]
# ]
#
#
#
from dataclasses import dataclass
import re, time
from typing import Mapping, Dict, List, Tuple, NamedTuple
from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import (
DiscoveryResult,
CheckResult,
StringTable,
)
from cmk.base.plugins.agent_based.agent_based_api.v1 import (
register,
Service,
Result,
check_levels,
State,
SNMPTree,
contains,
OIDEnd,
get_rate,
get_value_store,
Metric,
render,
)
@dataclass
class Section:
peer_prefixes: dict
peer_table: dict
###########################################################################
#
# DATA Parser function
#
###########################################################################
def parse_cisco_bgp_peer(string_table: List[StringTable]) -> Section:
def bgp_render_ipv4_address(bytestring):
return ".".join(["%s" % ord(m) for m in bytestring])
def bgp_shorten_ipv6_adress(address):
address = address.split(':')
span = 2
address = [''.join(address[i:i + span]) for i in range(0, len(address), span)]
for m in range(0, len(address)):
address[m] = re.sub(r'^0{1,3}', r'', address[m])
address = ':'.join(address)
zeros = ':0:0:0:0:0:0:'
while not zeros == '':
if zeros in address:
address = re.sub(r'%s' % zeros, r'::', address)
zeros = ''
else:
zeros = zeros[:-2]
return address
def bgp_render_ipv6_address(bytestring):
address = ":".join(["%02s" % hex(ord(m))[2:] for m in bytestring]).replace(' ', '0').upper()
address = bgp_shorten_ipv6_adress(address)
return address
def bgp_render_ip_address(bytestring):
if len(bytestring) == 4:
return bgp_render_ipv4_address(bytestring)
elif len(bytestring) == 16:
return bgp_render_ipv6_address(bytestring)
else:
return ''
def cisco_bgp_get_peer(OID_END):
# returns peer address string from OID_END
# u'1.4.217.119.208.34.1.1' --> 217.119.208.34
# u'2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.17.2.1' --> 42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.17
peer_ip = ''
OID_END = OID_END.split('.')
if int(len(OID_END)) == 7: # length of ip address
peer_ip = '.'.join(OID_END[3:7]) # ipv4 address
elif int(OID_END[1]) == 16: # ipv6 address
peer_ip = ':'.join('%02s' % hex(int(m))[2:] for m in OID_END[2:18]).replace(' ', '0').upper()
peer_ip = bgp_shorten_ipv6_adress(peer_ip)
return peer_ip
def cisco_bgp_errors(bytestring):
lasterrorhex = ''.join(["%02X " % ord(x) for x in bytestring]).strip()
byte1, byte2 = lasterrorhex.split()
names = {}
names[0] = {0: 'NO ERROR'}
names[1] = {0: 'Message',
2: 'Connection Not Synchronized',
3: 'Bad Message Length',
4: 'Bad Message Type',
}
names[2] = {0: 'OPEN',
1: 'Unsupported Version Number',
2: 'Bad Peer AS',
3: 'Bad BGP Identifier',
4: 'Unsupported Optional Parameter',
5: 'Authentication Failure',
6: 'Unacceptable Hold',
}
names[3] = {0: 'UPDATE',
1: 'Malformed Attribute List',
2: 'Unrecognized Well-known Attribute',
3: 'Missing Well-known Attribute',
4: 'Attribute Flags Error',
5: 'Attribute Length Error',
6: 'Invalid ORIGIN Attribute',
7: 'AS Routing Loop',
8: 'Invalid NEXT_HOP Attribute',
9: 'Optional Attribute Error',
10: 'Invalid Network Field',
11: 'Malformed AS_PATH',
}
names[4] = {0: 'Hold Timer Expired', }
names[5] = {0: 'Finite State Machine Error', }
names[6] = {0: 'Administratively Shutdown',
1: 'Max Prefix Reached',
2: 'Peer Unconfigured',
3: 'Administratively Reset',
4: 'Connection Rejected',
5: 'Other Configuration Change',
}
return names[int(byte1, 16)].get(int(byte2, 16))
# bgp not active
if not string_table == [[], [], []]:
cbgpPeer2Entry, cbgpPeer3Entry, cbgpPeer2AddrFamily = string_table
cbgpPeerEntry = cbgpPeer2Entry + cbgpPeer3Entry
peer_prefixes = {}
# create dictionary from cbgpPeer2AddrFamily ('remoteip addrfamilyname' as index)
if len(cbgpPeer2AddrFamily) > 0:
for entry in cbgpPeer2AddrFamily:
oid_end, addrfamilyname, acceptedprefixes, deniedprefixes, prefixadminlimit, prefixthreshold, \
prefixclearthreshold, advertisedprefixes, suppressedprefixes, withdrawnprefixes = entry
remoteaddr = cisco_bgp_get_peer(entry[0])
peer = {
'remoteaddr': remoteaddr,
'addrfamilyname': addrfamilyname
}
for key, value in [
('prefixadminlimit', prefixadminlimit),
('prefixthreshold', prefixthreshold),
('prefixclearthreshold', prefixclearthreshold),
('acceptedprefixes', acceptedprefixes),
('advertisedprefixes', advertisedprefixes),
('deniedprefixes', deniedprefixes),
('suppressedprefixes', suppressedprefixes),
('withdrawnprefixes', withdrawnprefixes),
]:
try:
peer[key] = int(value)
except ValueError:
pass
peer_prefixes.update({'%s %s' % (remoteaddr, addrfamilyname): peer})
# workaround: get remote ip from cbgpPeerEntry if cbgpPeer2AddrFamilyName is missing :-(
elif len(cbgpPeerEntry) > 0:
for entry in cbgpPeerEntry:
remoteaddr = cisco_bgp_get_peer(entry[0])
addrfamilyname = ''
peer = {'remoteaddr': remoteaddr, }
peer_prefixes.update({'%s %s' % (remoteaddr, addrfamilyname): peer})
# create dictionary from cbgpPeerEntry (peer ip address as index)
peer_table = {}
for entry in cbgpPeerEntry:
oid_end, state, adminstatus, localaddr, localas, localidentifier, remoteas, remoteidentifier, inupdates, \
outupdates, intotalmessages, outtotalmessages, lasterror, fsmestablishedtransitions, fsmestablishedtime, \
inupdateelapsedtime, lasterrortxt, prevstate = entry
peer = {'remoteaddr': cisco_bgp_get_peer(oid_end),
'localaddr': bgp_render_ip_address(localaddr),
'localid': localidentifier,
'remoteid': remoteidentifier,
'lasterror': cisco_bgp_errors(lasterror),
'lasterrortxt': lasterrortxt,
'prevstate': int(prevstate),
}
for key, value in [
('state', state),
('adminstate', adminstatus),
('localas', localas),
('remoteas', remoteas),
('inupdates', inupdates),
('outupdates', outupdates),
('intotalmessages', intotalmessages),
('outtotalmessages', outtotalmessages),
('fsmestablishedtransitions', fsmestablishedtransitions),
('fsmestablishedtime', fsmestablishedtime),
('inupdateelapsedtime', inupdateelapsedtime),
]:
try:
peer[key] = int(value)
except ValueError:
pass
peer_table.update({'%s' % cisco_bgp_get_peer(oid_end): peer})
return Section(
peer_prefixes=peer_prefixes,
peer_table=peer_table,
)
###########################################################################
#
# INVENTORY function
#
###########################################################################
def discovery_cisco_bgp_peer(section: Section) -> DiscoveryResult:
for key in section.peer_prefixes.keys():
yield Service(item=key)
###########################################################################
#
# CHECK function
#
###########################################################################
def check_cisco_bgp_peer(item, params, section) -> CheckResult:
def cisco_bgp_adminstate(state):
names = {1: 'stop',
2: 'start', }
return names.get(state, 'unknown (%s)' % state)
def cisco_bgp_peerstate(state):
names = {0: 'none',
1: 'idle',
2: 'connect',
3: 'active',
4: 'opensned',
5: 'openconfirm',
6: 'established'}
return names.get(state, 'unknown (%s)' % state)
peer_prefixes = section.peer_prefixes
peer_table = section.peer_table
prefixes = peer_prefixes.get(item, None)
alias = ''
peer_not_found_state = 3
for bgp_connection, bgp_alias, not_found_state in params.get('peer_list', []):
if item == bgp_connection:
alias = bgp_alias
peer_not_found_state = not_found_state
if prefixes:
longoutput = ''
peer = peer_table.get(prefixes.get('remoteaddr'))
if peer.get('localas') == 0:
peer.update({'localas': params.get('useaslocalas')})
if alias != '':
yield Result(state=State.OK, summary='Alias: %s' % alias)
peerstate = peer.get('state')
adminstate = peer.get('adminstate')
establishedtime = peer.get('fsmestablishedtime')
if peerstate == 1: # idle
yield Result(state=State.CRIT, summary='Peer state: %s' % cisco_bgp_peerstate(peerstate))
elif peerstate == 6: # established
yield from check_levels(
value=establishedtime,
label='Uptime',
levels_lower=params['minuptime'],
render_func=render.timespan
)
else: # everything else
yield Result(state=State.WARN, summary='Peer state: %s' % cisco_bgp_peerstate(peerstate))
if not adminstate == 2: # not start
yield Result(state=State.WARN, summary='Admin state: %s' % cisco_bgp_adminstate(adminstate))
for key, value in [
('remoteid', 'Remote ID: %s'),
('remoteas', 'Remote AS: %s'),
('localaddr', 'Local address: %s'),
('localid', 'Local ID: %s'),
('localas', 'Local AS: %s'),
]:
if key in params['infotext_values']:
try:
yield Result(state=State.OK, summary=value % peer[key])
except KeyError:
pass
bgptype = ''
if not peer.get('localas') == 0:
if peer.get('remoteas') == peer.get('localas'):
bgptype = ' (iBGP)'
else:
bgptype = ' (eBGP)'
longoutput_data = [
['IP-address (remote/local)', peer.get('remoteaddr'), peer.get('localaddr')],
['Router-ID (remote/local)', peer.get('remoteid'), peer.get('localid')],
['Autonomus System (remote/local)', peer.get('remoteas'), str(peer.get('localas')) + bgptype],
['State', cisco_bgp_peerstate(peerstate), ''],
['Admin state', cisco_bgp_adminstate(adminstate), ''],
['Last error', peer.get('lasterror'), ''],
['Last error text', peer.get('lasterrortxt'), ''],
['Previous state', cisco_bgp_peerstate(peer.get('prevstate')), ''],
['Address family name', prefixes.get('addrfamilyname', 'unknown'), ''],
['Prefix clear threshold (%)', '%.0d' % prefixes.get('prefixclearthreshold', 0), '']
,
]
acceptedprefixes = prefixes.get('acceptedprefixes', None)
prefixadminlimit = prefixes.get('prefixadminlimit', None)
prefixthreshold = prefixes.get('prefixthreshold', None)
if prefixadminlimit is not None and prefixthreshold is not None:
warnthreshold = prefixadminlimit / 100.0 * prefixthreshold # use float (100.0) to get xx.xx in division
longoutput_data.append(['Prefix admin limit (prefixes)', '%.0d' % prefixadminlimit, ''])
longoutput_data.append(['Prefix threshold (prefixes/%)', '%.0d' % warnthreshold, '%.0d' % prefixthreshold])
else:
yield Result(state=State(params['noprefixlimit']), notice='No admin prefix limit/warn threshold configured on the device.')
warnthreshold = None
if params.get('htmloutput', False):
#
# disable 'Escape HTML codes in plugin output' in wato --> global settings
#
table_bracket = '<table border="1">%s</table>'
line_bracket = '<tr>%s</tr>'
cell_bracket = '<td>%s</td><td>%s</td><td>%s</td>'
cell_seperator = ''
longoutput = '\n' + table_bracket % (''.join(
[line_bracket % cell_seperator.join([cell_bracket % (entry[0], entry[1], entry[2])]) for entry in
longoutput_data]))
else:
longoutput += '\nfor nicer output' \
'\ndisable \'Escape HTML codes in plugin output\' in wato -> global settings and enable HTML output in \'Parameters for this service\''
for entry in longoutput_data:
if not entry[2] == '':
longoutput += '\n{}: {} / {}'.format(entry[0], entry[1], entry[2])
else:
longoutput += '\n{}: {}'.format(entry[0], entry[1])
if prefixadminlimit is not None:
yield from check_levels(
value=acceptedprefixes,
metric_name='cisco_bgp_peer_acceptedprefixes',
levels_upper=(warnthreshold, prefixadminlimit),
label='Prefixes accepted',
render_func=lambda v: '%s' % str(v)
)
now_time = time.time()
value_store = get_value_store()
rate_item = item.replace(' ', '_').replace(':', '_')
for key in [
'deniedprefixes',
'advertisedprefixes',
'withdrawnprefixes',
'suppressedprefixes',
'inupdates',
'outupdates',
'intotalmessages',
'outtotalmessages',
]:
try:
value = get_rate(value_store, 'cisco_bgp_peer.%s.%s' % (key, rate_item), now_time, prefixes[key],
raise_overflow=False)
yield Metric(name='cisco_bgp_peer_%s' % key, value=value, boundaries=(0, None))
except KeyError:
pass
for key in [
'fsmestablishedtransitions',
'fsmestablishedtime',
'inupdateelapsedtime'
]:
try:
yield Metric(name='cisco_bgp_peer_%s' % key, value=peer[key], boundaries=(0, None))
except KeyError:
pass
yield Result(state=State.OK, notice=longoutput)
else:
if alias != '':
yield Result(state=State.OK, summary=', Alias: %s' % alias)
yield Result(state=State(peer_not_found_state), summary='Item not found in SNMP data')
###########################################################################
#
# CHECK info
#
###########################################################################
register.snmp_section(
name='cisco_bgp_peer',
parse_function=parse_cisco_bgp_peer,
fetch=[
SNMPTree(
base='.1.3.6.1.4.1.9.9.187.1.2.5.1', # CCISCO-BGP4-MIB::cbgpPeer2Entry
oids=[
OIDEnd(),
'3', # cbgpPeer2State
'4', # cbgpPeer2AdminStatus
'6', # cbgpPeer2LocalAddr
'8', # cbgpPeer2LocalAs -> empty
'9', # cbgpPeer2LocalIdentifier
'11', # cbgpPeer2RemoteAs
'12', # cbgpPeer2RemoteIdentifier
'13', # cbgpPeer2InUpdates
'14', # cbgpPeer2OutUpdates
'15', # cbgpPeer2InTotalMessages
'16', # cbgpPeer2OutTotalMessages
'17', # cbgpPeer2LastError
'18', # cbgpPeer2FsmEstablishedTransitions
'19', # cbgpPeer2FsmEstablishedTime
'27', # cbgpPeer2InUpdateElapsedTime
'28', # cbgpPeer2LastErrorTxt
'29', # cbgpPeer2PrevState
]
),
SNMPTree(
base='.1.3.6.1.4.1.9.9.187.1.2.9.1', # CCISCO-BGP4-MIB::cbgpPeer3Entry
oids=[
OIDEnd(),
'5', # cbgpPeer3State
'6', # cbgpPeer3AdminStatus
'8', # cbgpPeer3LocalAddr
'10', # cbgpPeer3LocalAs -> empty
'11', # cbgpPeer3LocalIdentifier
'13', # cbgpPeer3RemoteAs
'14', # cbgpPeer3RemoteIdentifier
'15', # cbgpPeer3InUpdates
'16', # cbgpPeer3OutUpdates
'17', # cbgpPeer3InTotalMessages
'18', # cbgpPeer3OutTotalMessages
'19', # cbgpPeer3LastError
'20', # cbgpPeer3FsmEstablishedTransitions
'21', # cbgpPeer3FsmEstablishedTime
'29', # cbgpPeer3InUpdateElapsedTime
'30', # cbgpPeer3LastErrorTxt
'31', # cbgpPeer3PrevState
]
),
SNMPTree(
base='.1.3.6.1.4.1.9.9.187.1.2', # cbgpPeer
oids=[
OIDEnd(), #
# .7.1 --> cbgpPeer2AddrFamilyEntry
'7.1.3', # cbgpPeer2AddrFamilyName
# .8.1 --> cbgpPeer2AddrFamilyPrefixEntry
'8.1.1', # cbgpPeer2AcceptedPrefixes
'8.1.2', # cbgpPeer2DeniedPrefixes
'8.1.3', # cbgpPeer2PrefixAdminLimit
'8.1.4', # cbgpPeer2PrefixThreshold
'8.1.5', # cbgpPeer2PrefixClearThreshold
'8.1.6', # cbgpPeer2AdvertisedPrefixes
'8.1.7', # cbgpPeer2SuppressedPrefixes
'8.1.8', # cbgpPeer2WithdrawnPrefixes
]
)
],
detect=contains('.1.3.6.1.2.1.1.1.0', 'Cisco'),
)
register.check_plugin(
name='cisco_bgp_peer',
service_name='Cisco BGP peer %s',
discovery_function=discovery_cisco_bgp_peer,
check_function=check_cisco_bgp_peer,
check_default_parameters={
'minuptime': (7200, 3600),
'useaslocalas': 0,
'htmloutput': False,
'noprefixlimit': 1,
'infotext_values': [],
'peer_list': [],
},
check_ruleset_name='cisco_bgp_peer',
)

View File

@ -0,0 +1,178 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# License: GNU General Public License v2
#
# Author: thl-cmk[at]outlook[dot]com
# URL : https://thl-cmk.hopto.org
# Date : 2017-12-26
#
# Cisco BGP Peer metrics plugin
#
# 2018-05-25: cleanup
# 2020-09-10: fixed typo FMS --> FSM (Thanks martin[dot]pechstein[at]posteo[dot]de)
# 2021-03-27: rewrite for CMK 2.0
#
from cmk.gui.i18n import _
from cmk.gui.plugins.metrics import (
metric_info,
graph_info,
perfometer_info
)
#####################################################################################################################
#
# define metrics for bgp peer perfdata
#
#####################################################################################################################
metric_info['cisco_bgp_peer_acceptedprefixes'] = {
'title': _('Prefixes accepted'),
'help': _('number of accepted prefixes'),
'unit': 'count',
'color': '11/a',
}
metric_info['cisco_bgp_peer_deniedprefixes'] = {
'title': _('Prefixes denied'),
'unit': '1/s',
'color': '21/a',
}
metric_info['cisco_bgp_peer_advertisedprefixes'] = {
'title': _('Prefixes advertised'),
'unit': '1/s',
'color': '31/a',
}
metric_info['cisco_bgp_peer_withdrawnprefixes'] = {
'title': _('Prefixes withdrawn'),
'unit': '1/s',
'color': '41/a',
}
metric_info['cisco_bgp_peer_suppressedprefixes'] = {
'title': _('Prefixes suppressed'),
'unit': '1/s',
'color': '12/a',
}
metric_info['cisco_bgp_peer_inupdates'] = {
'title': _('Updates received'),
'unit': '1/s',
'color': '22/a',
}
metric_info['cisco_bgp_peer_outupdates'] = {
'title': _('Updates send'),
'unit': '1/s',
'color': '32/a',
}
metric_info['cisco_bgp_peer_intotalmessages'] = {
'title': _('Total messages received'),
'unit': '1/s',
'color': '42/a',
}
metric_info['cisco_bgp_peer_outtotalmessages'] = {
'title': _('Total messages send'),
'unit': '1/s',
'color': '13/a',
}
metric_info['cisco_bgp_peer_fsmestablishedtransitions'] = {
'title': _('FSM transitions'),
'unit': 'count',
'color': '23/a',
}
metric_info['cisco_bgp_peer_fsmestablishedtime'] = {
'title': _('FSM last change'),
'unit': 's',
'color': '26/a',
}
metric_info['cisco_bgp_peer_inupdateelapsedtime'] = {
'title': _('Last update received'),
'unit': 's',
'color': '43/a',
}
######################################################################################################################
#
# how to graph perdata for bgp peer
#
######################################################################################################################
graph_info['cisco_bgp_peer.prefixes_accepted']={
'title': _('Accepted Prefixes'),
'metrics': [
('cisco_bgp_peer_acceptedprefixes', 'line'),
],
'scalars': [
('cisco_bgp_peer_acceptedprefixes:crit', _('crit')),
('cisco_bgp_peer_acceptedprefixes:warn', _('warn')),
],
}
graph_info['cisco_bgp_peer.prefixes_per_second']={
'title': _('Prefixes/s'),
'metrics': [
('cisco_bgp_peer_deniedprefixes', 'line'),
('cisco_bgp_peer_advertisedprefixes', 'line'),
('cisco_bgp_peer_withdrawnprefixes', 'line'),
('cisco_bgp_peer_suppressedprefixes', 'line'),
],
}
graph_info['cisco_bgp_peer.updates_in_out']={
'title': _('Updates'),
'metrics': [
('cisco_bgp_peer_inupdates', 'area'),
('cisco_bgp_peer_outupdates', '-area'),
]
}
graph_info['cisco_bgp_peer.messages_in_out']={
'title': _('Total messages'),
'metrics': [
('cisco_bgp_peer_intotalmessages', 'area'),
('cisco_bgp_peer_outtotalmessages', '-area'),
]
}
graph_info['cisco_bgp_peer.fms_transitions_from_to']={
'title': _('FSM transitions from/to established'),
'metrics': [
('cisco_bgp_peer_fsmestablishedtransitions', 'line'),
],
}
graph_info['cisco_bgp_peer.fms_transitions_last_change']={
'title': _('FSM established last change'),
'metrics': [
('cisco_bgp_peer_fsmestablishedtime', 'line'),
]
}
graph_info['cisco_bgp_peer.time_since_last_update']={
'title': _('Time since last update received'),
'metrics': [
('cisco_bgp_peer_inupdateelapsedtime', 'line'),
]
}
######################################################################################################################
#
# define perf-o-meter for bgp peer uptime + prefixes accepted/advertised
#
######################################################################################################################
perfometer_info.append(('stacked', [
{
'type': 'logarithmic',
'metric': 'cisco_bgp_peer_fsmestablishedtime',
'half_value': 2592000.0, # ome month
'exponent': 2,
},
{
'type': 'logarithmic',
'metric': 'cisco_bgp_peer_acceptedprefixes',
'half_value': 500000.0,
'exponent': 2,
}
]))

View File

@ -0,0 +1,126 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# License: GNU General Public License v2
#
# Author: thl-cmk[at]outlook[dot]com
# URL : https://thl-cmk.hopto.org
# Date : 2017-12-25
#
# Check_MK cisco_bgp_peers WATO plugin
#
# 2021-03-27: rewrite for CMK 2.0
#
from cmk.gui.i18n import _
from cmk.gui.valuespec import (
Dictionary,
Integer,
TextAscii,
ListOfStrings,
FixedValue,
ListChoice,
ListOf,
Tuple,
TextUnicode,
MonitoringState,
)
from cmk.gui.plugins.wato import (
CheckParameterRulespecWithItem,
rulespec_registry,
RulespecGroupCheckParametersNetworking,
)
cisco_bgp_peer_infotext_values = [
('remoteid', 'Remote router ID'),
('remoteas', 'Remote autonomous system'),
('localaddr', 'Local peer IP address'),
('localid', 'Local router ID'),
('localas', 'Local autonomous system'),
]
def _parameter_valuespec_cisco_bgp_peer():
return Dictionary(elements=[
('minuptime',
Tuple(
title=_('Minimum uptime for peer'),
help=_('Set the time in seconds, a peer must be up before the peer is considered sable.'
'If the peer uptime less then X, the check outcome is set to warning.'),
elements=[
Integer(title=_('Warning if below'), unit='seconds', default_value=7200, minvalue=0),
Integer(title=_('Critical if below'), unit='seconsa', default_value=3600, minvalue=0)
],
),
),
('useaslocalas',
Integer(
help=_('Use this AS number if the SNMP Value for CISCO-BGP4-MIB::cbgpPeer2LocalAs is \'0\'.'),
title=_('Use AS as local AS, if SNMP cbgpPeer2LocalAs is not valid.'),
default_value=0,
# allow_empty=False,
),
),
('htmloutput',
FixedValue(
True,
help=_('render long output of check plugin (multiline) as HTML table. Needs \'Escape HTML codes in plugin output\' in wato --> global settings disabled'),
title=_('enable HTML Output for long output of check plugin (multiline)'),
totext=_('enable HTML Output for long output of check plugin (multiline)'),
default_value=False,
)),
('noprefixlimit',
MonitoringState(
default_value=1,
title=_('State if no admin prefix limit/warn threshold configured.'),
help=_('The admin prefix limit and warn threshold needs to be configured on the device. '
'For example: neighbor 172.17.10.10 maximum-prefix 10000 80. The threshold is in percentage '
'of the prefix limit')
)),
('infotext_values',
ListChoice(
title=_('Add values to check info'),
help=_('Select values to add to the check output.'),
choices=cisco_bgp_peer_infotext_values,
default_value=[],
)),
('peer_list',
ListOf(
Tuple(
# title=('BGP Peers'),
elements=[
TextUnicode(
title=_('BGP Peer item name (without "Cisco BGP peer")'),
help=_('The configured value must match a BGP item reported by the monitored '
'device. For example: "10.194.115.98 IPv4 Unicast"'),
allow_empty=False,
),
TextUnicode(
title=_('BGP Peer Alias'),
help=_('You can configure an individual alias here for the BGP peer matching '
'the text configured in the "BGP Peer item name" field. The alias will '
'be shown in the infotext'),
),
MonitoringState(
default_value=2,
title=_('State if not found'),
help=_('You can configure an individual state if the BGP peer matching the text '
'configured in the "BGP Peer item name" field is not found')
),
]),
add_label=_('Add BGP peer'),
movable=False,
title=_('BGP Peers'),
)),
])
rulespec_registry.register(
CheckParameterRulespecWithItem(
check_group_name='cisco_bgp_peer',
group=RulespecGroupCheckParametersNetworking,
item_spec=lambda: TextAscii(title=_('BGP peer specific configuration'), ),
match_type='dict',
parameter_valuespec=_parameter_valuespec_cisco_bgp_peer,
title=lambda: _('Cisco BGP peer'),
))

1
cisco-gdoi/README.md Normal file
View File

@ -0,0 +1 @@
This plugin checks the registration status (and KEK key timeout) of GDOI Group Members with GDOI Key Servers.

BIN
cisco-gdoi/cisco_gdoi-1.1.0.mkp Executable file

Binary file not shown.

View File

@ -0,0 +1,98 @@
#!/usr/bin/env python3
#
# More information about this Cisco system:
# https://www.cisco.com/en/US/docs/ios-xml/ios/sec_conn_getvpn/configuration/15-2mt/sec-get-vpn.html
#
from dataclasses import dataclass
from typing import Dict, List
from cmk.base.plugins.agent_based.agent_based_api.v1 import (
register,
Service,
Result,
State,
SNMPTree,
contains,
OIDEnd,
)
@dataclass
class Section:
kek_info: dict
def chars_to_ip_addr(chars):
return ".".join(map(lambda c: str(ord(c)), [*chars]))
conversions = {
"1": "using",
"2": "new",
"3": "old",
}
# SNMP parsing function
def parse_cisco_gdoi(string_table):
def parse(data):
lookup = {}
for val in data:
ip = chars_to_ip_addr(val[0])
remaining = int(val[1])
state = conversions[val[2]]
lookup.setdefault(ip, {})
lookup[ip][state] = remaining
return lookup
if string_table == [[]]:
return
return Section(
kek_info=parse(string_table[0]),
)
# Inventory function, returning inventory based upon SNMP parsed result above
def discovery_cisco_gdoi(section):
yield Service(item="Keyservers", parameters=section.kek_info)
# Check function, returning ok/crit based upon SNMP parsed result above
def check_cisco_gdoi(item, params, section):
state = params
registered = False
for ip, state in params.items():
in_use = state.get("using")
if in_use > 0:
registered = True
yield Result(state=State.OK, summary="Registered, using KEK from " + ip)
if not registered:
yield Result(state=State.CRIT, summary="Unregistered")
register.snmp_section(
name="cisco_gdoi",
parse_function=parse_cisco_gdoi,
fetch=[
SNMPTree(
# ciscoGdoiMIB::cgmGdoiGmKekRemainingLifetime
base=".1.3.6.1.4.1.9.9.759.1.3.2.1",
oids=[
"5", # cgmGdoiGmKekSrcIdValue
"20", # cgmGdoiGmKekRemainingLifetime
"21", # cgmGdoiGmKekStatus
]
),
],
detect=contains(".1.3.6.1.2.1.1.1.0", "Cisco"),
)
register.check_plugin(
name="cisco_gdoi",
service_name="Cisco GDOI %s",
discovery_function=discovery_cisco_gdoi,
check_function=check_cisco_gdoi,
check_default_parameters={},
check_ruleset_name="cisco_gdoi",
)

Binary file not shown.

View File

@ -0,0 +1,208 @@
#!/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 cmk.gui.i18n import _
from cmk.gui.plugins.wato.utils import (
CheckParameterRulespecWithItem,
rulespec_registry,
RulespecGroupCheckParametersNetworking,
)
from cmk.gui.valuespec import Dictionary, DropdownChoice, Integer, TextInput, Tuple
def _item_spec_cisco_ip_sla():
return TextInput(
title=_("RTT row index of the service"),
allow_empty=True,
)
def _parameter_valuespec_cisco_ip_sla():
return Dictionary(
elements=[
(
"rtt_type",
DropdownChoice(
title=_("RTT type"),
choices=[
("echo", _("echo")),
("path echo", _("path echo")),
("file IO", _("file IO")),
("UDP echo", _("UDP echo")),
("TCP connect", _("TCP connect")),
("HTTP", _("HTTP")),
("DNS", _("DNS")),
("jitter", _("jitter")),
("DLSw", _("DLSw")),
("DHCP", _("DHCP")),
("FTP", _("FTP")),
("VoIP", _("VoIP")),
("RTP", _("RTP")),
("LSP group", _("LSP group")),
("ICMP jitter", _("ICMP jitter")),
("LSP ping", _("LSP ping")),
("LSP trace", _("LSP trace")),
("ethernet ping", _("ethernet ping")),
("ethernet jitter", _("ethernet jitter")),
("LSP ping pseudowire", _("LSP ping pseudowire")),
],
default_value="echo",
),
),
(
"threshold",
Integer(
title=_("Treshold"),
help=_(
"Depending on the precision the unit can be "
"either milliseconds or micoseconds."
),
unit=_("ms/us"),
minvalue=1,
default_value=5000,
),
),
(
"state",
DropdownChoice(
title=_("State"),
choices=[
("active", _("active")),
("inactive", _("inactive")),
("reset", _("reset")),
("orderly stop", _("orderly stop")),
("immediate stop", _("immediate stop")),
("pending", _("pending")),
("restart", _("restart")),
],
default_value="active",
),
),
(
"connection_lost_occured",
DropdownChoice(
title=_("Connection lost occured"),
choices=[
("yes", _("yes")),
("no", _("no")),
],
default_value="no",
),
),
(
"timeout_occured",
DropdownChoice(
title=_("Timeout occured"),
choices=[
("yes", _("yes")),
("no", _("no")),
],
default_value="no",
),
),
(
"completion_time_over_treshold_occured",
DropdownChoice(
title=_("Completion time over treshold occured"),
choices=[
("yes", _("yes")),
("no", _("no")),
],
default_value="no",
),
),
(
"latest_rtt_completion_time",
Tuple(
title=_("Latest RTT completion time"),
help=_(
"Depending on the precision the unit can be "
"either milliseconds or micoseconds."
),
elements=[
Integer(
title=_("Warning at"),
unit=_("ms/us"),
minvalue=1,
default_value=100,
),
Integer(
title=_("Critical at"),
unit=_("ms/us"),
minvalue=1,
default_value=200,
),
],
),
),
(
"latest_rtt_state",
DropdownChoice(
title=_("Latest RTT state"),
choices=[
("ok", _("OK")),
("disconnected", _("disconnected")),
("over treshold", _("over treshold")),
("timeout", _("timeout")),
("other", _("other")),
],
default_value="ok",
),
),
(
"packets_lost_src->dest",
Tuple(
title=_("Packets lost src->dest"),
elements=[
Integer(
title=_("Warning at"),
unit=_("packets"),
minvalue=1,
default_value=100,
),
Integer(
title=_("Critical at"),
unit=_("packets"),
minvalue=1,
default_value=1000,
),
],
),
),
(
"packets_lost_dest->src",
Tuple(
title=_("Packets lost dest->src"),
elements=[
Integer(
title=_("Warning at"),
unit=_("packets"),
minvalue=1,
default_value=100,
),
Integer(
title=_("Critical at"),
unit=_("packets"),
minvalue=1,
default_value=1000,
),
],
),
),
],
)
rulespec_registry.register(
CheckParameterRulespecWithItem(
check_group_name="cisco_ip_sla",
group=RulespecGroupCheckParametersNetworking,
item_spec=_item_spec_cisco_ip_sla,
match_type="dict",
parameter_valuespec=_parameter_valuespec_cisco_ip_sla,
title=lambda: _("Cisco IP SLA"),
)
)

View File

@ -0,0 +1,237 @@
#!/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.
factory_settings["cisco_ip_sla_default_levels"] = {
"state": "active",
"connection_lost_occured": "no",
"timeout_occured": "no",
"completion_time_over_treshold_occured": "no",
"latest_rtt_completion_time": (250, 500),
"latest_rtt_state": "ok",
"packets_lost_src->dest": (100, 1000),
"packets_lost_dest->src": (100, 1000),
}
def parse_cisco_ip_sla(info):
precisions = {line[0]: "ms" if line[-1] == "1" else "us" for line in info[0]}
rtt_types = {
"1": "echo",
"2": "path echo",
"3": "file IO",
"4": "script",
"5": "UDP echo",
"6": "TCP connect",
"7": "HTTP",
"8": "DNS",
"9": "jitter",
"10": "DLSw",
"11": "DHCP",
"12": "FTP",
"13": "VoIP",
"14": "RTP",
"15": "LSP group",
"16": "ICMP jitter",
"17": "LSP ping",
"18": "LSP trace",
"19": "ethernet ping",
"20": "ethernet jitter",
"21": "LSP ping pseudowire",
}
states = {
"1": "reset",
"2": "orderly stop",
"3": "immediate stop",
"4": "pending",
"5": "inactive",
"6": "active",
"7": "restart",
}
rtt_states = {
"0": "other",
"1": "ok",
"2": "disconnected",
"3": "over threshold",
"4": "timeout",
"5": "busy",
"6": "not connected",
"7": "dropped",
"8": "sequence error",
"9": "verify error",
"10": "application specific error",
}
def to_ip_address(int_list):
if len(int_list) == 4:
return "%d.%d.%d.%d" % tuple(int_list)
elif len(int_list) == 6:
return "%d:%d:%d:%d:%d:%d" % tuple(int_list)
return ""
# contains description, parse function, unit and type
contents = [
( # rttMonEchoAdminEntry
("Target address", to_ip_address, "", None),
("Source address", to_ip_address, "", None),
# rttMonEchoAdminPrecision is deliberatly dropped by zip below
),
( # rttMonCtrlAdminEntry
("Owner", None, "", None),
("Tag", None, "", None),
("RTT type", lambda x: rtt_types.get(x, "unknown"), "", "option"),
("Threshold", int, "ms", "option"),
),
( # rttMonCtrlOperEntry
("State", lambda x: states.get(x, "unknown"), "", "option"),
("Text", None, "", None),
("Connection lost occured", lambda x: "yes" if x == "1" else "no", "", "option"),
("Timeout occured", lambda x: "yes" if x == "1" else "no", "", "option"),
(
"Completion time over treshold occured",
lambda x: "yes" if x == "1" else "no",
"",
"option",
),
),
( # rttMonLatestRttOperEntry
("Latest RTT completion time", int, "ms/us", "level"),
("Latest RTT state", lambda x: rtt_states.get(x, "unknown"), "", "option"),
),
( # rttMonJitterStatsEntry
("Packets lost src->dest", int, "packets", "level"),
("Packets lost dest->src", int, "packets", "level"),
),
]
parsed = {}
for content, entries in zip(contents, info):
if not entries:
continue
for entry in entries:
index, values = entry[0], entry[1:]
data = parsed.setdefault(index, [])
for (description, parser, unit, type_), value in zip(content, values):
if parser:
value = parser(value)
if unit == "ms/us":
unit = precisions[index]
data.append((description, value, unit, type_))
return parsed
def inventory_cisco_ip_sla(parsed):
for index in parsed:
yield index, {}
@get_parsed_item_data
def check_cisco_ip_sla(_item, params, data):
for description, value, unit, type_ in data:
if not value and "packets" not in unit:
continue
state = 0
if unit:
infotext = "%s: %s %s" % (description, value, unit)
else:
infotext = "%s: %s" % (description, value)
perfdata = []
param = params.get(description.lower().replace(" ", "_"))
if type_ == "option":
if param and param != value:
state = 1
infotext += " (expected %s)" % param
elif type_ == "level":
warn, crit = param # a default level hat to exist
if value >= crit:
state = 2
elif value >= warn:
state = 1
if state:
infotext += " (warn/crit at %s/%s)" % (warn, crit)
if unit == "ms" or unit == "us" or unit == "ms/us":
factor = 1e3 if unit == "ms" else 1e6
perfdata = [
("rtt", value / factor, warn / factor, crit / factor)
] # fixed: true-division
elif unit == "packets":
perfdata = [
("lost", value, warn, crit)
]
yield state, infotext, perfdata
check_info["cisco_ip_sla"] = {
"parse_function": parse_cisco_ip_sla,
"inventory_function": inventory_cisco_ip_sla,
"check_function": check_cisco_ip_sla,
"service_description": "Cisco IP SLA %s",
"group": "cisco_ip_sla",
"default_levels_variable": "cisco_ip_sla_default_levels",
"has_perfdata": True,
"snmp_scan_function": lambda oid: "cisco" in oid(".1.3.6.1.2.1.1.1.0").lower()
and "os" in oid(".1.3.6.1.2.1.1.1.0").lower()
# and oid(".1.3.6.1.4.1.9.9.42.1.2.2.1.37.*"),
"snmp_info": [
(
".1.3.6.1.4.1.9.9.42.1.2.2.1",
[
OID_END,
BINARY(2), # rttMonEchoAdminTargetAddress
BINARY(6), # rttMonEchoAdminSourceAddress
# only needed to determine the unit (ms/us)
37, # rttMonEchoAdminPrecision
],
),
(
".1.3.6.1.4.1.9.9.42.1.2.1.1",
[
OID_END,
2, # rttMonCtrlAdminOwner
3, # rttMonCtrlAdminTag
4, # rttMonCtrlAdminRttType
5, # rttMonCtrlAdminThreshold
],
),
(
".1.3.6.1.4.1.9.9.42.1.2.9.1",
[
OID_END,
10, # rttMonCtrlOperState
2, # rttMonCtrlOperDiagText
5, # rttMonCtrlOperConnectionLostOccurred
6, # rttMonCtrlOperTimeoutOccurred
7, # rttMonCtrlOperOverThresholdOccurred
],
),
(
".1.3.6.1.4.1.9.9.42.1.2.10.1",
[
OID_END,
1, # rttMonLatestRttOperCompletionTime
2, # rttMonLatestRttOperSense
],
),
(
".1.3.6.1.4.1.9.9.42.1.5.2.1",
[
OID_END,
26, # rttMonLatestJitterOperPacketLossSD
27, # rttMonLatestJitterOperPacketLossDS
]
)
],
}

Binary file not shown.

View File

@ -0,0 +1,208 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 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.gui.i18n import _
from cmk.gui.plugins.wato.utils import (
CheckParameterRulespecWithItem,
rulespec_registry,
RulespecGroupCheckParametersNetworking,
)
from cmk.gui.valuespec import Dictionary, DropdownChoice, Integer, TextInput, Tuple
def _item_spec_cisco_ip_sla():
return TextInput(
title=_("RTT row index of the service"),
allow_empty=True,
)
def _parameter_valuespec_cisco_ip_sla():
return Dictionary(
elements=[
(
"rtt_type",
DropdownChoice(
title=_("RTT type"),
choices=[
("echo", _("echo")),
("path echo", _("path echo")),
("file IO", _("file IO")),
("UDP echo", _("UDP echo")),
("TCP connect", _("TCP connect")),
("HTTP", _("HTTP")),
("DNS", _("DNS")),
("jitter", _("jitter")),
("DLSw", _("DLSw")),
("DHCP", _("DHCP")),
("FTP", _("FTP")),
("VoIP", _("VoIP")),
("RTP", _("RTP")),
("LSP group", _("LSP group")),
("ICMP jitter", _("ICMP jitter")),
("LSP ping", _("LSP ping")),
("LSP trace", _("LSP trace")),
("ethernet ping", _("ethernet ping")),
("ethernet jitter", _("ethernet jitter")),
("LSP ping pseudowire", _("LSP ping pseudowire")),
],
default_value="echo",
),
),
(
"threshold",
Integer(
title=_("Treshold"),
help=_(
"Depending on the precision the unit can be "
"either milliseconds or micoseconds."
),
unit=_("ms/us"),
minvalue=1,
default_value=5000,
),
),
(
"state",
DropdownChoice(
title=_("State"),
choices=[
("active", _("active")),
("inactive", _("inactive")),
("reset", _("reset")),
("orderly stop", _("orderly stop")),
("immediate stop", _("immediate stop")),
("pending", _("pending")),
("restart", _("restart")),
],
default_value="active",
),
),
(
"connection_lost_occured",
DropdownChoice(
title=_("Connection lost occured"),
choices=[
("yes", _("yes")),
("no", _("no")),
],
default_value="no",
),
),
(
"timeout_occured",
DropdownChoice(
title=_("Timeout occured"),
choices=[
("yes", _("yes")),
("no", _("no")),
],
default_value="no",
),
),
(
"completion_time_over_treshold_occured",
DropdownChoice(
title=_("Completion time over treshold occured"),
choices=[
("yes", _("yes")),
("no", _("no")),
],
default_value="no",
),
),
(
"latest_rtt_completion_time",
Tuple(
title=_("Latest RTT completion time"),
help=_(
"Depending on the precision the unit can be "
"either milliseconds or micoseconds."
),
elements=[
Integer(
title=_("Warning at"),
unit=_("ms/us"),
minvalue=1,
default_value=100,
),
Integer(
title=_("Critical at"),
unit=_("ms/us"),
minvalue=1,
default_value=200,
),
],
),
),
(
"latest_rtt_state",
DropdownChoice(
title=_("Latest RTT state"),
choices=[
("ok", _("OK")),
("disconnected", _("disconnected")),
("over treshold", _("over treshold")),
("timeout", _("timeout")),
("other", _("other")),
],
default_value="ok",
),
),
(
"packets_lost_src->dest",
Tuple(
title=_("Packets lost src->dest"),
elements=[
Integer(
title=_("Warning at"),
unit=_("packets"),
minvalue=1,
default_value=100,
),
Integer(
title=_("Critical at"),
unit=_("packets"),
minvalue=1,
default_value=1000,
),
],
),
),
(
"packets_lost_dest->src",
Tuple(
title=_("Packets lost dest->src"),
elements=[
Integer(
title=_("Warning at"),
unit=_("packets"),
minvalue=1,
default_value=100,
),
Integer(
title=_("Critical at"),
unit=_("packets"),
minvalue=1,
default_value=1000,
),
],
),
),
],
)
rulespec_registry.register(
CheckParameterRulespecWithItem(
check_group_name="cisco_ip_sla",
group=RulespecGroupCheckParametersNetworking,
item_spec=_item_spec_cisco_ip_sla,
match_type="dict",
parameter_valuespec=_parameter_valuespec_cisco_ip_sla,
title=lambda: _("Cisco IP SLA"),
)
)

View File

@ -0,0 +1,225 @@
#!/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 Callable, Sequence
from cmk.base.check_api import LegacyCheckDefinition
from cmk.base.config import check_info
from cmk.agent_based.v2 import all_of, contains, exists, OIDBytes, OIDEnd, SNMPTree
def parse_cisco_ip_sla(string_table):
precisions = {line[0]: "ms" if line[-1] == "1" else "us" for line in string_table[0]}
rtt_types = {
"1": "echo",
"2": "path echo",
"3": "file IO",
"4": "script",
"5": "UDP echo",
"6": "TCP connect",
"7": "HTTP",
"8": "DNS",
"9": "jitter",
"10": "DLSw",
"11": "DHCP",
"12": "FTP",
"13": "VoIP",
"14": "RTP",
"15": "LSP group",
"16": "ICMP jitter",
"17": "LSP ping",
"18": "LSP trace",
"19": "ethernet ping",
"20": "ethernet jitter",
"21": "LSP ping pseudowire",
}
states = {
"1": "reset",
"2": "orderly stop",
"3": "immediate stop",
"4": "pending",
"5": "inactive",
"6": "active",
"7": "restart",
}
rtt_states = {
"0": "other",
"1": "ok",
"2": "disconnected",
"3": "over threshold",
"4": "timeout",
"5": "busy",
"6": "not connected",
"7": "dropped",
"8": "sequence error",
"9": "verify error",
"10": "application specific error",
}
def to_ip_address(int_list):
if len(int_list) == 4:
return "%d.%d.%d.%d" % tuple(int_list)
elif len(int_list) == 6:
return "%d:%d:%d:%d:%d:%d" % tuple(int_list)
return ""
# contains description, parse function, unit and type
contents: Sequence[tuple[tuple[str, Callable | None, str, str | None], ...]] = [
( # rttMonEchoAdminEntry
("Target address", to_ip_address, "", None),
("Source address", to_ip_address, "", None),
# rttMonEchoAdminPrecision is deliberatly dropped by zip below
),
( # rttMonCtrlAdminEntry
("Owner", None, "", None),
("Tag", None, "", None),
("RTT type", lambda x: rtt_types.get(x, "unknown"), "", "option"),
("Threshold", int, "ms", "option"),
),
( # rttMonCtrlOperEntry
("State", lambda x: states.get(x, "unknown"), "", "option"),
("Text", None, "", None),
(
"Connection lost occured",
lambda x: "yes" if x == "1" else "no",
"",
"option",
),
("Timeout occured", lambda x: "yes" if x == "1" else "no", "", "option"),
(
"Completion time over treshold occured",
lambda x: "yes" if x == "1" else "no",
"",
"option",
),
),
( # rttMonLatestRttOperEntry
("Latest RTT completion time", int, "ms/us", "level"),
("Latest RTT state", lambda x: rtt_states.get(x, "unknown"), "", "option"),
),
( # rttMonJitterStatsEntry
("Packets lost src->dest", int, "packets", "level"),
("Packets lost dest->src", int, "packets", "level"),
),
]
parsed: dict[str, list] = {}
for content, entries in zip(contents, string_table):
if not entries:
continue
for entry in entries:
index, values = entry[0], entry[1:]
data = parsed.setdefault(index, [])
for (description, parser, unit, type_), value in zip(content, values):
if parser:
value = parser(value)
if unit == "ms/us":
unit = precisions[index]
data.append((description, value, unit, type_))
return parsed
def inventory_cisco_ip_sla(parsed):
for index in parsed:
yield index, {}
def check_cisco_ip_sla(item, params, parsed):
if not (data := parsed.get(item)):
return
for description, value, unit, type_ in data:
if not value and "packets" not in unit:
continue
state = 0
if unit:
infotext = f"{description}: {value} {unit}"
else:
infotext = f"{description}: {value}"
perfdata = []
param = params.get(description.lower().replace(" ", "_"))
if type_ == "option":
if param and param != value:
state = 1
infotext += " (expected %s)" % param
elif type_ == "level":
warn, crit = param # a default level hat to exist
if value >= crit:
state = 2
elif value >= warn:
state = 1
if state:
infotext += f" (warn/crit at {warn}/{crit})"
# last check ("ms/us") is probably a harmless bug, but keeping
if unit == "ms" or unit == "us" or unit == "ms/us":
factor = 1e3 if unit == "ms" else 1e6
perfdata = [
("rtt", value / factor, warn / factor, crit / factor)
] # fixed: true-division
elif unit == "packets":
perfdata = [
("lost", value, warn, crit)
]
yield state, infotext, perfdata
check_info["cisco_ip_sla"] = LegacyCheckDefinition(
detect=all_of(
contains(".1.3.6.1.2.1.1.1.0", "cisco"),
contains(".1.3.6.1.2.1.1.1.0", "ios"),
exists(".1.3.6.1.4.1.9.9.42.1.2.2.1.37.*"),
),
fetch=[
SNMPTree(
base=".1.3.6.1.4.1.9.9.42.1.2.2.1",
oids=[OIDEnd(), OIDBytes("2"), OIDBytes("6"), "37"],
),
SNMPTree(
base=".1.3.6.1.4.1.9.9.42.1.2.1.1",
oids=[OIDEnd(), "2", "3", "4", "5"],
),
SNMPTree(
base=".1.3.6.1.4.1.9.9.42.1.2.9.1",
oids=[OIDEnd(), "10", "2", "5", "6", "7"],
),
SNMPTree(
base=".1.3.6.1.4.1.9.9.42.1.2.10.1",
oids=[OIDEnd(), "1", "2"],
),
SNMPTree(
base=".1.3.6.1.4.1.9.9.42.1.5.2.1",
# rttMonLatestJitterOperPacketLossSD
# rttMonLatestJitterOperPacketLossDS
oids=[OIDEnd(), "26", "27"],
),
],
parse_function=parse_cisco_ip_sla,
service_name="Cisco IP SLA %s",
discovery_function=inventory_cisco_ip_sla,
check_function=check_cisco_ip_sla,
check_ruleset_name="cisco_ip_sla",
check_default_parameters={
"state": "active",
"connection_lost_occured": "no",
"timeout_occured": "no",
"completion_time_over_treshold_occured": "no",
"latest_rtt_completion_time": (250, 500),
"latest_rtt_state": "ok",
"packets_lost_src->dest": (100, 1000),
"packets_lost_dest->src": (100, 1000),
},
)

1
cisco-ip-sla/README.md Normal file
View File

@ -0,0 +1 @@
This is a modification of a Tribe29 GPL plugin to support tracking packet loss as well.

20
clever-pdu/cmk1.6/README Normal file
View File

@ -0,0 +1,20 @@
{'author': u'George Pochiscan',
'description': u'',
'download_url': '',
'files': {'checkman': ['clever_pdu',
'clever_pdu_humidity',
'clever_pdu_temp'],
'checks': ['clever_pdu_120',
'clever_pdu_130',
'clever_pdu_humidity_120',
'clever_pdu_humidity_130',
'clever_pdu_temp_120',
'clever_pdu_temp_130'],
'web': ['plugins/wato/clever_pdu.py']},
'name': 'clever_pdu_1-6',
'num_files': 10,
'title': u'Clever PDU checks for Checkmk 1.6',
'version': '1.1',
'version.min_required': '1.6.0p20',
'version.packaged': '1.6.0p29',
'version.usable_until': '2.0.0p1'}

Binary file not shown.

View File

@ -0,0 +1,13 @@
title: Clever PDU Units: Power and Voltage
agents: snmp
catalog: hw/power/clever
license: GPLv2
distribution: check_mk
description:
Monitors Power, Voltage and energy on Clever PDU Units.
item:
ID of the Line.
discovery:
One service is created for each Line.

View File

@ -0,0 +1,13 @@
title: Clever PDU Units: Master Humidity
agents: snmp
catalog: hw/power/clever
license: GPLv2
distribution: check_mk
description:
Monitors Master Humidity on Clever PDU Units.
item:
Master Humidity.
discovery:
One service is created.

View File

@ -0,0 +1,13 @@
title: Clever PDU Units: Master Temperature
agents: snmp
catalog: hw/power/clever
license: GPLv2
distribution: check_mk
description:
Monitors Master Temperature on Clever PDU Units.
item:
Master Temperature.
discovery:
One service is created.

View File

@ -0,0 +1,129 @@
#!/usr/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# | ____ _ _ __ __ _ __ |
# | / ___| |__ ___ ___| | __ | \/ | |/ / |
# | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
# | | |___| | | | __/ (__| < | | | | . \ |
# | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
# | |
# | Copyright Mathias Kettner 2016 mk@mathias-kettner.de |
# +------------------------------------------------------------------+
#
# This file is part of Check_MK.
# The official homepage is at http://mathias-kettner.de/check_mk.
#
# check_mk is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation in version 2. check_mk is distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
# out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU General Public License for more de-
# tails. You should have received a copy of the GNU General Public
# License along with GNU Make; see the file COPYING. If not, write
# to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301 USA.
factory_settings["clever_pdu_default_levels"] ={
"voltage": (240, 250),
"current": (32, 33),
"energy": (35000, 36000),
}
lines = {"Line 1", "Line 2", "Line 3"}
_UNIT_MAP = {
"voltage": "V" ,
"current": "A" ,
"energy": "W",
}
def parse_clever_pdu_120(info):
data=info[0]
parsed = {}
parsed = {
"Line 1" : {
"voltage": float(data[0]),
"current": float(data[3])/10,
"energy": float(data[6]),
},
"Line 2" : {
"voltage": float(data[1]),
"current": float(data[4])/10,
"energy": float(data[7]),
},
"Line 3" : {
"voltage": float(data[2]),
"current": float(data[5])/10,
"energy": float(data[8]),
},
"Total Energy" : {
"energy" : float((float(data[0])*float(data[3])/10)) + float((float(data[1])*float(data[4])/10)) + float((float(data[2])*float(data[5])/10)),
},
}
return parsed
def inventory_clever_pdu_120(parsed):
for line in parsed:
yield line, {}
def check_clever_pdu_120(item, params, parsed):
if "Total" not in item:
for param in params:
levels_lower = levels_upper = None
warn, crit = params.get(param)
if warn > crit:
levels_lower = warn, crit
else:
levels_upper = warn, crit
yield check_levels(
parsed.get(item).get(param),
param,
(warn, crit),
unit = _UNIT_MAP.get(param),
infoname = param
)
else:
for param in params:
if "energy" in param:
levels_lower = levels_upper = None
warn, crit = params.get(param)
if warn > crit:
levels_lower = warn, crit
else:
levels_upper = warn, crit
yield check_levels(
parsed.get(item).get(param),
param,
(warn, crit),
unit = _UNIT_MAP.get(param),
infoname = param
)
check_info['clever_pdu_120'] = {
'parse_function' : parse_clever_pdu_120,
'inventory_function' : inventory_clever_pdu_120,
'check_function' : check_clever_pdu_120,
'service_description' : '%s',
'has_perfdata' : True,
'group' : "clever_pdu",
'snmp_info' : ('.1.3.6.1.4.1.30966.10.3.2',
[
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
],
),
'snmp_scan_function' : lambda oid: oid(".1.3.6.1.2.1.1.2.0").startswith(".1.3.6.1.4.1.30966") and oid(".1.3.6.1.4.1.30966.10.3.2.70.0"),
'default_levels_variable' : 'clever_pdu_default_levels',
}

View File

@ -0,0 +1,127 @@
#!/usr/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# | ____ _ _ __ __ _ __ |
# | / ___| |__ ___ ___| | __ | \/ | |/ / |
# | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
# | | |___| | | | __/ (__| < | | | | . \ |
# | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
# | |
# | Copyright Mathias Kettner 2016 mk@mathias-kettner.de |
# +------------------------------------------------------------------+
#
# This file is part of Check_MK.
# The official homepage is at http://mathias-kettner.de/check_mk.
#
# check_mk is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation in version 2. check_mk is distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
# out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU General Public License for more de-
# tails. You should have received a copy of the GNU General Public
# License along with GNU Make; see the file COPYING. If not, write
# to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301 USA.
factory_settings["clever_pdu_default_levels"] ={
"voltage": (240, 250),
"current": (32, 33),
"energy": (35000, 36000),
}
lines = {"Line 1", "Line 2", "Line 3"}
_UNIT_MAP = {
"voltage": "V" ,
"current": "A" ,
"energy": "W",
}
def parse_clever_pdu(info):
data=info[0]
parsed = {}
parsed = {
"Line 1" : {
"voltage": float(data[0]),
"current": float(float(data[3])/10),
"energy" : float((float(data[0])*float(data[3])/10)),
},
"Line 2" : {
"voltage": float(data[1]),
"current": float(float(data[4])/10),
"energy" : float((float(data[1])*float(data[4])/10)),
},
"Line 3" : {
"voltage": float(data[2]),
"current": float(float(data[5])/10),
"energy" : float((float(data[2])*float(data[5])/10)),
},
"Total Energy" : {
"energy" : float((float(data[0])*float(data[3])/10)) + float((float(data[1])*float(data[4])/10)) + float((float(data[2])*float(data[5])/10)),
},
}
return parsed
def inventory_clever_pdu(parsed):
for line in parsed:
yield line, {}
def check_clever_pdu(item, params, parsed):
if "Total" not in item:
for param in params:
levels_lower = levels_upper = None
warn, crit = params.get(param)
if warn > crit:
levels_lower = warn, crit
else:
levels_upper = warn, crit
yield check_levels(
parsed.get(item).get(param),
param,
(warn, crit),
unit = _UNIT_MAP.get(param),
infoname = param
)
else:
for param in params:
if "energy" in param:
levels_lower = levels_upper = None
warn, crit = params.get(param)
if warn > crit:
levels_lower = warn, crit
else:
levels_upper = warn, crit
yield check_levels(
parsed.get(item).get(param),
param,
(warn, crit),
unit = _UNIT_MAP.get(param),
infoname = param
)
check_info['clever_pdu'] = {
'parse_function' : parse_clever_pdu,
'inventory_function' : inventory_clever_pdu,
'check_function' : check_clever_pdu,
'service_description' : '%s',
'has_perfdata' : True,
'group' : "clever_pdu",
'snmp_info' : ('.1.3.6.1.4.1.30966.10.3.2',
[
'1',
'2',
'3',
'4',
'5',
'6',
],
),
'snmp_scan_function' : lambda oid: oid(".1.3.6.1.2.1.1.2.0").startswith(".1.3.6.1.4.1.30966") and not oid(".1.3.6.1.4.1.30966.10.3.2.70.0"),
'default_levels_variable' : 'clever_pdu_default_levels',
}

View File

@ -0,0 +1,49 @@
#!/usr/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# | ____ _ _ __ __ _ __ |
# | / ___| |__ ___ ___| | __ | \/ | |/ / |
# | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
# | | |___| | | | __/ (__| < | | | | . \ |
# | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
# | |
# | Copyright Mathias Kettner 2016 mk@mathias-kettner.de |
# +------------------------------------------------------------------+
#
# This file is part of Check_MK.
# The official homepage is at http://mathias-kettner.de/check_mk.
#
# check_mk is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation in version 2. check_mk is distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
# out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU General Public License for more de-
# tails. You should have received a copy of the GNU General Public
# License along with GNU Make; see the file COPYING. If not, write
# to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301 USA.
factory_settings["clever_pdu_humidity_default_levels"] = {
"levels": (60, 70),
}
def inventory_clever_pdu_humidity_120(info):
yield "Master humidity", {}
def check_clever_pdu_humidity_120(item, params, info):
return check_humidity(float(info[0][0]), params)
check_info['clever_pdu_humidity_120'] = {
'inventory_function' : inventory_clever_pdu_humidity_120,
'check_function' : check_clever_pdu_humidity_120,
'service_description' : '%s',
'has_perfdata' : True,
'snmp_info' : ('.1.3.6.1.4.1.30966.10.3.2.14', ['0']),
'snmp_scan_function' : lambda oid: oid(".1.3.6.1.2.1.1.2.0").startswith(".1.3.6.1.4.1.30966") and oid(".1.3.6.1.4.1.30966.10.3.2.70.0"),
'group' : 'humidity',
'default_levels_variable' : 'clever_pdu_humidity_default_levels',
'includes' : ['humidity.include'],
}

View File

@ -0,0 +1,49 @@
#!/usr/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# | ____ _ _ __ __ _ __ |
# | / ___| |__ ___ ___| | __ | \/ | |/ / |
# | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
# | | |___| | | | __/ (__| < | | | | . \ |
# | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
# | |
# | Copyright Mathias Kettner 2016 mk@mathias-kettner.de |
# +------------------------------------------------------------------+
#
# This file is part of Check_MK.
# The official homepage is at http://mathias-kettner.de/check_mk.
#
# check_mk is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation in version 2. check_mk is distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
# out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU General Public License for more de-
# tails. You should have received a copy of the GNU General Public
# License along with GNU Make; see the file COPYING. If not, write
# to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301 USA.
factory_settings["clever_pdu_humidity_default_levels"] = {
"levels": (60, 70),
}
def inventory_clever_pdu_humidity(info):
yield "Master humidity", {}
def check_clever_pdu_humidity(item, params, info):
return check_humidity(float(info[0][0]), params)
check_info['clever_pdu_humidity'] = {
'inventory_function' : inventory_clever_pdu_humidity,
'check_function' : check_clever_pdu_humidity,
'service_description' : '%s',
'has_perfdata' : True,
'snmp_info' : ('.1.3.6.1.4.1.30966.10.3.2.11', ['0']),
'snmp_scan_function' : lambda oid: oid(".1.3.6.1.2.1.1.2.0").startswith(".1.3.6.1.4.1.30966") and not oid(".1.3.6.1.4.1.30966.10.3.2.70.0"),
'group' : 'humidity',
'default_levels_variable' : 'clever_pdu_humidity_default_levels',
'includes' : ['humidity.include'],
}

View File

@ -0,0 +1,49 @@
#!/usr/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# | ____ _ _ __ __ _ __ |
# | / ___| |__ ___ ___| | __ | \/ | |/ / |
# | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
# | | |___| | | | __/ (__| < | | | | . \ |
# | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
# | |
# | Copyright Mathias Kettner 2016 mk@mathias-kettner.de |
# +------------------------------------------------------------------+
#
# This file is part of Check_MK.
# The official homepage is at http://mathias-kettner.de/check_mk.
#
# check_mk is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation in version 2. check_mk is distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
# out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU General Public License for more de-
# tails. You should have received a copy of the GNU General Public
# License along with GNU Make; see the file COPYING. If not, write
# to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301 USA.
factory_settings["clever_pdu_temp_default_levels"] = {
"levels": (60, 70),
}
def inventory_clever_pdu_temp_120(info):
yield "Master Temperature", {}
def check_clever_pdu_temp_120(item, params, info):
return check_temperature(float(info[0][0]), params, "Master Temperature %s" %item)
check_info['clever_pdu_temp_120'] = {
'inventory_function' : inventory_clever_pdu_temp_120,
'check_function' : check_clever_pdu_temp_120,
'service_description' : '%s',
'has_perfdata' : True,
'snmp_info' : ('.1.3.6.1.4.1.30966.10.3.2.13', ['0']),
'snmp_scan_function' : lambda oid: oid(".1.3.6.1.2.1.1.2.0").startswith(".1.3.6.1.4.1.30966") and oid(".1.3.6.1.4.1.30966.10.3.2.70.0"),
'group' : 'temperature',
'default_levels_variable' : 'clever_pdu_temp_default_levels',
'includes' : ['temperature.include'],
}

View File

@ -0,0 +1,49 @@
#!/usr/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# | ____ _ _ __ __ _ __ |
# | / ___| |__ ___ ___| | __ | \/ | |/ / |
# | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
# | | |___| | | | __/ (__| < | | | | . \ |
# | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
# | |
# | Copyright Mathias Kettner 2016 mk@mathias-kettner.de |
# +------------------------------------------------------------------+
#
# This file is part of Check_MK.
# The official homepage is at http://mathias-kettner.de/check_mk.
#
# check_mk is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation in version 2. check_mk is distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
# out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU General Public License for more de-
# tails. You should have received a copy of the GNU General Public
# License along with GNU Make; see the file COPYING. If not, write
# to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301 USA.
factory_settings["clever_pdu_temp_default_levels"] = {
"levels": (60, 70),
}
def inventory_clever_pdu_temp(info):
yield "Master Temperature", {}
def check_clever_pdu_temp(item, params, info):
return check_temperature(float(info[0][0]), params, "Master Temperature %s" %item)
check_info['clever_pdu_temp'] = {
'inventory_function' : inventory_clever_pdu_temp,
'check_function' : check_clever_pdu_temp,
'service_description' : '%s',
'has_perfdata' : True,
'snmp_info' : ('.1.3.6.1.4.1.30966.10.3.2.10', ['0']),
'snmp_scan_function' : lambda oid: oid(".1.3.6.1.2.1.1.2.0").startswith(".1.3.6.1.4.1.30966") and not oid(".1.3.6.1.4.1.30966.10.3.2.70.0"),
'group' : 'temperature',
'default_levels_variable' : 'clever_pdu_temp_default_levels',
'includes' : ['temperature.include'],
}

View File

@ -0,0 +1,83 @@
#!/usr/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# | ____ _ _ __ __ _ __ |
# | / ___| |__ ___ ___| | __ | \/ | |/ / |
# | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
# | | |___| | | | __/ (__| < | | | | . \ |
# | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
# | |
# | Copyright Mathias Kettner 2014 mk@mathias-kettner.de |
# +------------------------------------------------------------------+
#
# This file is part of Check_MK.
# The official homepage is at http://mathias-kettner.de/check_mk.
#
# check_mk is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation in version 2. check_mk is distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
# out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU General Public License for more de-
# tails. You should have received a copy of the GNU General Public
# License along with GNU Make; see the file COPYING. If not, write
# to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301 USA.
from cmk.gui.i18n import _
from cmk.gui.plugins.wato import (
CheckParameterRulespecWithItem,
rulespec_registry,
RulespecGroupCheckParametersEnvironment,
)
from cmk.gui.valuespec import Dictionary, Integer, TextAscii, Tuple
def _parameter_valuespec_clever_pdu():
return Dictionary(
elements=[
(
"voltage",
Tuple(
title=_("Voltage on Line"),
elements=[
Integer(title=_("warning at"), unit=_("V")),
Integer(title=_("critical at"), unit=_("V")),
],
),
),
(
"current",
Tuple(
title=_("Current on Power Channel"),
elements=[
Integer(title=_("warning if below"), unit=_("A")),
Integer(title=_("critical if below"), unit=_("A")),
],
),
),
(
"energy",
Tuple(
title=_("Active Energy of Line"),
elements=[
Integer(title=_("warning at"), unit=_("W")),
Integer(title=_("critical at"), unit=_("W")),
],
),
),
],
)
rulespec_registry.register(
CheckParameterRulespecWithItem(
check_group_name="clever_pdu",
group=RulespecGroupCheckParametersEnvironment,
item_spec=lambda: TextAscii(title=_("Line"),),
match_type="dict",
parameter_valuespec=_parameter_valuespec_clever_pdu,
title=lambda: _("Levels for Clever AC PDU Devices"),
)
)

View File

@ -0,0 +1,13 @@
title: Clever PDU Units: Power and Voltage
agents: snmp
catalog: hw/power/clever
license: GPLv2
distribution: check_mk
description:
Monitors Power, Voltage and energy on Clever PDU Units.
item:
ID of the Line.
discovery:
One service is created for each Line.

View File

@ -0,0 +1,13 @@
title: Clever PDU Units: Master Humidity
agents: snmp
catalog: hw/power/clever
license: GPLv2
distribution: check_mk
description:
Monitors Master Humidity on Clever PDU Units.
item:
Master Humidity.
discovery:
One service is created.

View File

@ -0,0 +1,13 @@
title: Clever PDU Units: Master Temperature
agents: snmp
catalog: hw/power/clever
license: GPLv2
distribution: check_mk
description:
Monitors Master Temperature on Clever PDU Units.
item:
Master Temperature.
discovery:
One service is created.

Binary file not shown.

20
clever-pdu/cmk2/info Normal file
View File

@ -0,0 +1,20 @@
{'author': 'George Pochiscan',
'description': 'Ported Clever AC PDU from 2.1.0 checkmk version to 2.0.0 '
'checkmk version.\n',
'download_url': '',
'files': {'agent_based': ['utils/humidity.py',
'clever_pdu_120.py',
'clever_pdu_130.py',
'clever_pdu_humidity_120.py',
'clever_pdu_humidity_130.py',
'clever_pdu_temp_120.py',
'clever_pdu_temp_130.py'],
'checkman': ['clever_pdu', 'clever_pdu_humidity', 'clever_pdu_temp'],
'web': ['plugins/wato/clever_pdu.py']},
'name': 'clever_pdu_2',
'num_files': 11,
'title': 'Clever PDU checks for 2.0 checkmk version',
'version': '1.0.1',
'version.min_required': '2.0.0p20',
'version.packaged': '2.0.0p29',
'version.usable_until': '2.1.0p20'}

View File

@ -0,0 +1,126 @@
#!/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 dataclasses import dataclass
from typing import Iterable, Mapping, Tuple, NamedTuple
from .agent_based_api.v1 import check_levels, equals, register, Service, SNMPTree, all_of, exists
from .agent_based_api.v1.type_defs import CheckResult, DiscoveryResult, StringTable
def parse_clever_pdu_120(string_table: StringTable):
data=string_table[0]
parsed = {}
parsed = {
"Line 1" : {
"voltage": float(data[0]),
"current": float(data[3])/10,
"energy": float(data[6]),
},
"Line 2" : {
"voltage": float(data[1]),
"current": float(data[4])/10,
"energy": float(data[7]),
},
"Line 3" : {
"voltage": float(data[2]),
"current": float(data[5])/10,
"energy": float(data[8]),
},
"Total Energy" : {
"energy" : float((float(data[0])*float(data[3])/10)) + float((float(data[1])*float(data[4])/10)) + float((float(data[2])*float(data[5])/10)),
},
}
return parsed
_UNIT_MAP = {
"voltage": "V" ,
"current": "A" ,
"energy": "W",
}
register.snmp_section(
name="clever_pdu_120",
parsed_section_name="clever_pdu_120",
parse_function=parse_clever_pdu_120,
detect = all_of(
equals(
".1.3.6.1.2.1.1.2.0",
".1.3.6.1.4.1.30966",
),
exists(".1.3.6.1.4.1.30966.10.3.2.70.0"),
),
fetch=SNMPTree(
".1.3.6.1.4.1.30966.10.3.2",
[
"1", # mVoltageA
"2", # mVoltageB
"3", # mVoltageC
"4", # mCurrentA
"5", # mCurrentB
"6", # mCurrentC
"7", # mEnergyA
"8", # mEnergyB
"9", # mEnergyC
],
),
)
def discover_clever_pdu_120(section) -> DiscoveryResult:
yield from (Service(item=line_num) for line_num in section)
def check_clever_pdu_120(item, params, section) -> CheckResult:
if "Total" not in item:
for param in params:
levels_lower = levels_upper = None
warn, crit = params.get(param)
if warn > crit:
levels_lower = warn, crit
else:
levels_upper = warn, crit
yield from check_levels(
section.get(item)[param],
levels_upper = levels_upper,
levels_lower = levels_lower,
metric_name = param,
render_func=lambda v: f"{v:.2f} {_UNIT_MAP[param]}",
label = param,
)
else:
for param in params:
if "energy" in param:
levels_lower = levels_upper = None
warn, crit = params.get(param)
if warn > crit:
levels_lower = warn, crit
else:
levels_upper = warn, crit
yield from check_levels(
section.get(item)[param],
levels_upper = levels_upper,
levels_lower = levels_lower,
metric_name = param,
render_func=lambda v: f"{v:.2f} {_UNIT_MAP[param]}",
label = param,
)
register.check_plugin(
name="clever_pdu_120",
service_name="%s",
discovery_function=discover_clever_pdu_120,
check_function=check_clever_pdu_120,
check_ruleset_name="clever_pdu",
check_default_parameters={
"voltage": (220, 210),
"current": (32, 33),
"energy": (35000, 36000),
},
)

View File

@ -0,0 +1,125 @@
#!/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 dataclasses import dataclass
from typing import Iterable, Mapping, Tuple, NamedTuple
from .agent_based_api.v1 import check_levels, equals, register, Service, SNMPTree, all_of, not_exists
from .agent_based_api.v1.type_defs import CheckResult, DiscoveryResult, StringTable
def parse_clever_pdu_130(string_table: StringTable):
data=string_table[0]
parsed = {}
parsed = {
"Line 1" : {
"voltage": float(data[0]),
"current": float(data[3])/10,
"energy" : float((float(data[0])*float(data[3])/10)),
},
"Line 2" : {
"voltage": float(data[1]),
"current": float(data[4])/10,
"energy" : float((float(data[1])*float(data[4])/10)),
},
"Line 3" : {
"voltage": float(data[2]),
"current": float(data[5])/10,
"energy" : float((float(data[2])*float(data[5])/10)),
},
"Total Energy" : {
"energy" : float((float(data[0])*float(data[3])/10)) + float((float(data[1])*float(data[4])/10)) + float((float(data[2])*float(data[5])/10)),
},
}
return parsed
lines = {"Line 1", "Line 2", "Line 3"}
_UNIT_MAP = {
"voltage": "V" ,
"current": "A" ,
"energy": "W",
}
register.snmp_section(
name="clever_pdu_130",
parsed_section_name="clever_pdu_130",
parse_function=parse_clever_pdu_130,
detect = all_of(
equals(
".1.3.6.1.2.1.1.2.0",
".1.3.6.1.4.1.30966",
),
not_exists(".1.3.6.1.4.1.30966.10.3.2.70.0"),
),
fetch=SNMPTree(
".1.3.6.1.4.1.30966.10.3.2",
[
"1", # mVoltageA
"2", # mVoltageB
"3", # mVoltageC
"4", # mCurrentA
"5", # mCurrentB
"6", # mCurrentC
],
),
)
def discover_clever_pdu_130(section) -> DiscoveryResult:
yield from (Service(item=line_num) for line_num in section)
def check_clever_pdu_130(item, params, section) -> CheckResult:
if "Total" not in item:
for param in params:
levels_lower = levels_upper = None
warn, crit = params.get(param)
if warn > crit:
levels_lower = warn, crit
else:
levels_upper = warn, crit
yield from check_levels(
section.get(item)[param],
levels_upper = levels_upper,
levels_lower = levels_lower,
metric_name = param,
render_func=lambda v: f"{v:.2f} {_UNIT_MAP[param]}",
label = param,
)
else:
for param in params:
if "energy" in param:
levels_lower = levels_upper = None
warn, crit = params.get(param)
if warn > crit:
levels_lower = warn, crit
else:
levels_upper = warn, crit
yield from check_levels(
section.get(item)[param],
levels_upper = levels_upper,
levels_lower = levels_lower,
metric_name = param,
render_func=lambda v: f"{v:.2f} {_UNIT_MAP[param]}",
label = param,
)
register.check_plugin(
name="clever_pdu_130",
service_name="%s",
discovery_function=discover_clever_pdu_130,
check_function=check_clever_pdu_130,
check_ruleset_name="clever_pdu",
check_default_parameters={
"voltage": (220, 210),
"current": (32, 33),
"energy": (35000, 36000),
},
)

View File

@ -0,0 +1,64 @@
#!/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 typing import Mapping, Any
from .agent_based_api.v1 import check_levels, equals, register, Service, SNMPTree, get_value_store, all_of, exists
from .agent_based_api.v1.type_defs import CheckResult, DiscoveryResult, StringTable
from .utils.humidity import check_humidity
CheckParams = Mapping[str, Any]
def parse_clever_pdu_humidity_120(string_table: StringTable):
data=string_table[0]
parsed = {}
parsed = {
"Master Humidity" : int(data[0]),
}
return parsed
register.snmp_section(
name="clever_pdu_humidity_120",
parsed_section_name="clever_pdu_humidity_120",
parse_function=parse_clever_pdu_humidity_120,
detect=all_of(
equals(
".1.3.6.1.2.1.1.2.0",
".1.3.6.1.4.1.30966",
),
exists(".1.3.6.1.4.1.30966.10.3.2.70.0"),
),
fetch=SNMPTree(
".1.3.6.1.4.1.30966.10.3.2",
[
"14", # mHumidity
],
),
)
def discover_clever_pdu_humidity_120(section) -> DiscoveryResult:
if section.get("Master Humidity") == 0:
return
else:
yield from (Service(item=item) for item in section)
def check_clever_pdu_humidity_120(item, params: CheckParams, section) -> CheckResult:
yield from check_humidity(
section.get("Master Humidity"),
params,
)
register.check_plugin(
name="clever_pdu_humidity_120",
service_name="%s",
discovery_function=discover_clever_pdu_humidity_120,
check_function=check_clever_pdu_humidity_120,
check_ruleset_name="humidity",
check_default_parameters={},
)

View File

@ -0,0 +1,64 @@
#!/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 typing import Mapping, Any
from .agent_based_api.v1 import check_levels, equals, register, Service, SNMPTree, get_value_store, all_of, not_exists
from .agent_based_api.v1.type_defs import CheckResult, DiscoveryResult, StringTable
from .utils.humidity import check_humidity
CheckParams = Mapping[str, Any]
def parse_clever_pdu_humidity(string_table: StringTable):
data=string_table[0]
parsed = {}
parsed = {
"Master Humidity" : int(data[0]),
}
return parsed
register.snmp_section(
name="clever_pdu_humidity",
parsed_section_name="clever_pdu_humidity",
parse_function=parse_clever_pdu_humidity,
detect=all_of(
equals(
".1.3.6.1.2.1.1.2.0",
".1.3.6.1.4.1.30966",
),
not_exists(".1.3.6.1.4.1.30966.10.3.2.70.0"),
),
fetch=SNMPTree(
".1.3.6.1.4.1.30966.10.3.2",
[
"11", # mHumidity
],
),
)
def discover_clever_pdu_humidity(section) -> DiscoveryResult:
if section.get("Master Humidity") == 0:
return
else:
yield from (Service(item=item) for item in section)
def check_clever_pdu_humidity(item, params: CheckParams, section) -> CheckResult:
yield from check_humidity(
section.get("Master Humidity"),
params,
)
register.check_plugin(
name="clever_pdu_humidity",
service_name="%s",
discovery_function=discover_clever_pdu_humidity,
check_function=check_clever_pdu_humidity,
check_ruleset_name="humidity",
check_default_parameters={},
)

View File

@ -0,0 +1,69 @@
#!/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 dataclasses import dataclass
from typing import Iterable, Mapping, Tuple, NamedTuple
from .agent_based_api.v1 import check_levels, equals, register, Service, SNMPTree, get_value_store, all_of, exists
from .agent_based_api.v1.type_defs import CheckResult, DiscoveryResult, StringTable
from .utils.temperature import check_temperature, TempParamDict
def parse_clever_pdu_temp_120(string_table: StringTable):
data=string_table[0]
parsed = {}
parsed = {
"Master Temperature" : int(data[0]),
}
return parsed
register.snmp_section(
name="clever_pdu_temp_120",
parsed_section_name="clever_pdu_temp_120",
parse_function=parse_clever_pdu_temp_120,
detect = all_of(
equals(
".1.3.6.1.2.1.1.2.0",
".1.3.6.1.4.1.30966",
),
exists(".1.3.6.1.4.1.30966.10.3.2.70.0"),
),
fetch=SNMPTree(
".1.3.6.1.4.1.30966.10.3.2",
[
"13", # mTemperature
],
),
)
def discover_clever_pdu_temp_120(section) -> DiscoveryResult:
if section.get("Master Temperature") == 0:
return
else:
yield from (Service(item=item) for item in section)
def check_clever_pdu_temp_120(item, params: TempParamDict, section) -> CheckResult:
if (temperature := section.get(item)) is None:
return
yield from check_temperature(
reading=temperature,
params=params,
unique_name=item,
value_store=get_value_store(),
)
register.check_plugin(
name="clever_pdu_temp_120",
service_name="%s",
discovery_function=discover_clever_pdu_temp_120,
check_function=check_clever_pdu_temp_120,
check_ruleset_name="temperature",
check_default_parameters={},
)

View File

@ -0,0 +1,69 @@
#!/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 dataclasses import dataclass
from typing import Iterable, Mapping, Tuple, NamedTuple
from .agent_based_api.v1 import check_levels, equals, register, Service, SNMPTree, get_value_store, all_of, not_exists
from .agent_based_api.v1.type_defs import CheckResult, DiscoveryResult, StringTable
from .utils.temperature import check_temperature, TempParamDict
def parse_clever_pdu_temp(string_table: StringTable):
data=string_table[0]
parsed = {}
parsed = {
"Master Temperature" : int(data[0]),
}
return parsed
register.snmp_section(
name="clever_pdu_temp",
parsed_section_name="clever_pdu_temp",
parse_function=parse_clever_pdu_temp,
detect = all_of(
equals(
".1.3.6.1.2.1.1.2.0",
".1.3.6.1.4.1.30966",
),
not_exists(".1.3.6.1.4.1.30966.10.3.2.70.0"),
),
fetch=SNMPTree(
".1.3.6.1.4.1.30966.10.3.2",
[
"10", # mTemperature
],
),
)
def discover_clever_pdu_temp(section) -> DiscoveryResult:
if section.get("Master Temperature") == 0:
return
else:
yield from (Service(item=item) for item in section)
def check_clever_pdu_temp(item, params: TempParamDict, section) -> CheckResult:
if (temperature := section.get(item)) is None:
return
yield from check_temperature(
reading=temperature,
params=params,
unique_name=item,
value_store=get_value_store(),
)
register.check_plugin(
name="clever_pdu_temp",
service_name="%s",
discovery_function=discover_clever_pdu_temp,
check_function=check_clever_pdu_temp,
check_ruleset_name="temperature",
check_default_parameters={},
)

View File

@ -0,0 +1,33 @@
#!/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 typing import Any, List, Mapping, Optional, Tuple, Union
from ..agent_based_api.v1 import check_levels, render, type_defs
CheckParams = Union[
None, Mapping[str, Any], Optional[List[float]], Tuple[float, float, float, float]
]
def check_humidity(humidity: float, params: CheckParams) -> type_defs.CheckResult:
levels_upper, levels_lower = None, None
if isinstance(params, dict):
levels_upper = params.get("levels") or None
levels_lower = params.get("levels_lower") or None
elif isinstance(params, (list, tuple)):
# old params = (crit_low , warn_low, warn, crit)
levels_upper = params[2], params[3]
levels_lower = params[1], params[0]
yield from check_levels(
humidity,
levels_upper=levels_upper,
levels_lower=levels_lower,
metric_name="humidity",
render_func=render.percent,
boundaries=(0, 100),
)

View File

@ -0,0 +1,65 @@
#!/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 cmk.gui.i18n import _
from cmk.gui.plugins.wato.utils import (
CheckParameterRulespecWithItem,
rulespec_registry,
RulespecGroupCheckParametersEnvironment,
)
from cmk.gui.valuespec import Dictionary, Integer, TextInput, Tuple
def _parameter_valuespec_clever_pdu():
return Dictionary(
elements=[
(
"voltage",
Tuple(
title=_("Voltage on Line"),
elements=[
Integer(title=_("warning at"), unit=_("V")),
Integer(title=_("critical at"), unit=_("V")),
],
),
),
(
"current",
Tuple(
title=_("Current on Power Channel"),
elements=[
Integer(title=_("warning if below"), unit=_("A")),
Integer(title=_("critical if below"), unit=_("A")),
],
),
),
(
"energy",
Tuple(
title=_("Active Energy of Line"),
elements=[
Integer(title=_("warning at"), unit=_("W")),
Integer(title=_("critical at"), unit=_("W")),
],
),
),
],
)
rulespec_registry.register(
CheckParameterRulespecWithItem(
check_group_name="clever_pdu",
group=RulespecGroupCheckParametersEnvironment,
item_spec=lambda: TextInput(
title=_("Line"), help=_("The Line Number. Example: 'Line 1'.")
),
match_type="dict",
parameter_valuespec=_parameter_valuespec_clever_pdu,
title=lambda: _("Levels for Clever AC PDU Devices"),
)
)

View File

@ -0,0 +1,68 @@
#!/usr/bin/env python3
#
# Parses and checks non-phone devices from CUCM.
import json
from cmk.base.plugins.agent_based.agent_based_api.v1 import register, Result, Service, State
# Convert JSON entries into dictionaries indexed by name.
def parse_cucm(string_table):
lookup = {}
for row in string_table:
device = json.loads(row[0])
name = device["name"]
lookup[name] = device
return lookup
register.agent_section(
name="cucm_chk",
parse_function=parse_cucm
)
# Produce a list of services based on the parsed devices.
def discover_cucm(section):
for name, details in sorted(section.items()):
model_name = details["model_name"]
ip = details.get("ip")
gui_name = "%s %s (%s)" % (model_name, name, ip)
yield Service(item=gui_name, parameters={"name": name})
# Given a specific device, look it up in the parsed devices, and produce
# results on that service based upon the devices' status.
def check_cucm(item, params, section):
name = params["name"]
device = section.get(name)
if device is None:
yield Result(state=State.WARN, summary="Not appearing in CUCM API")
return
status = device.get("status")
if status is None:
yield Result(state=State.WARN, summary="No status for this in CUCM API")
elif status == "Registered":
yield Result(state=State.OK, summary="Registered")
elif ["Unregistered", "Rejected", "PartiallyRegistered", "Unknown"].count(status) == 1:
summary = status
msg = device.get("status_reason")
if msg:
summary += " " + msg
yield Result(state=State.WARN, summary=summary)
else:
yield Result(state=State.WARN, summary="Unknown status: %s" % status)
register.check_plugin(
name="cucm_chk",
service_name="CUCM %s",
discovery_function=discover_cucm,
check_function=check_cucm,
check_default_parameters={},
check_ruleset_name="cucm_chk",
)

View File

@ -0,0 +1,44 @@
#!/usr/bin/env python3
# Parses and inventories phones.
# XXX for the inventory plugin, if mac/serial/model is None, do not update the inventory
# XXX for checkmk, add a last_seen. If last_seen is older than six months, remove it.
import json
from cmk.base.plugins.agent_based.agent_based_api.v1 import register, TableRow
# Convert JSON entries into dictionaries indexed by name.
def parse_cucm(string_table):
lookup = {}
for row in string_table:
phone = json.loads(row[0])
name = phone["name"]
lookup[name] = phone
return lookup
# Produce a table of all phones parsed earlier.
def inventory_cucm(section):
path = ["phones"]
for name, details in sorted(section.items()):
details.pop("name")
yield TableRow(
path=path,
key_columns={"name": name},
inventory_columns=details
)
register.agent_section(
name="cucm_inv",
parse_function=parse_cucm
)
register.inventory_plugin(
name="cucm_inv",
inventory_function=inventory_cucm,
)

View File

@ -0,0 +1,340 @@
#!/usr/bin/env python3
#
# Contact CUCM to fetch the status of non-phone devices, and return the results
# of each device as a JSON line.
#
# This file imports code from agent_cucm_inv, so much of the important logic
# is found there.
from importlib.util import spec_from_loader, module_from_spec
from importlib.machinery import SourceFileLoader
import os, sys, json, urllib.request
# Since Python doesn't import files without .py extensions, we need to do a
# little tapdance to import agent_cucm_inv.
file = 'agent_cucm_inv'
path = os.path.dirname(__file__) + '/' + file
spec = spec_from_loader(file, SourceFileLoader(file, path))
inv = module_from_spec(spec)
spec.loader.exec_module(inv)
# Call the CUCM RisPort70 API synchronously, using a SOAP query to fetch
# information about devices matching the requested device type. It returns
# XML, which we parse.
#
# Be aware that the API will return information about a maximum of 2000 devices,
# and provides no means of pagination. Having more than 2000 non-phone devices
# would be quite exceptional, so we don't handle that here, but if you ever
# need to support more than that look into how agent_cucm_inv uses AXL to
# do pagination.
#
# See query_cucm_risport() in agent_cucm_inv for more info.
def query_cucm(addr, port, user, password, insecure, device):
url = 'https://%s:%s/realtimeservice2/services/RISService70/' % (addr, port)
headers = [('Content-Type', 'text/plain')]
try:
return inv.get_url(url, user, password, insecure, headers, f"""
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:soap="http://schemas.cisco.com/ast/soap">
<soapenv:Header/>
<soapenv:Body>
<soap:selectCmDevice>
<soap:StateInfo></soap:StateInfo>
<soap:CmSelectionCriteria>
<soap:MaxReturnedDevices>2000</soap:MaxReturnedDevices>
<soap:DeviceClass>{device}</soap:DeviceClass>
<soap:Model>255</soap:Model>
<soap:Status></soap:Status>
<soap:NodeName></soap:NodeName>
<soap:SelectBy>Name</soap:SelectBy>
<soap:SelectItems></soap:SelectItems>
<soap:Protocol>Any</soap:Protocol>
<soap:DownloadStatus>Any</soap:DownloadStatus>
</soap:CmSelectionCriteria>
</soap:selectCmDevice>
</soapenv:Body>
</soapenv:Envelope>
""")
except urllib.error.HTTPError as e:
sys.stderr.write("CUCM error: %s\n" % e)
# Statuses listed here: https://developer.cisco.com/docs/sxml/#!risport70-api-reference/ReasonCode
status_reason_lookup = {
"0": None,
"1": "Unknown",
"6": "ConnectivityError",
"8": "DeviceInitiatedReset",
"9": "CallManagerReset",
"10": "DeviceUnregistered",
"11": "MalformedRegisterMsg",
"12": "SCCPDeviceThrottling",
"13": "KeepAliveTimeout",
"14": "ConfigurationMismatch",
"15": "CallManagerRestart",
"16": "DuplicateRegistration",
"17": "CallManagerApplyConfig",
"18": "DeviceNoResponse",
"19": "EMLoginLogout",
"20": "EMCCLoginLogout",
"25": "RegistrationSequenceError",
"26": "InvalidCapabilities",
"28": "FallbackInitiated",
"29": "DeviceSwitch",
"30": "DeviceWipe",
"31": "DeviceForcedReset",
"33": "LowBattery",
"34": "ManualPowerOff",
}
# Model names listed here: https://developer.cisco.com/docs/sxml/#!risport70-api-reference/risport70-api-reference
model_name_lookup = {
"1": "Cisco 30 SP+",
"2": "Cisco 12 SP+",
"3": "Cisco 12 SP",
"4": "Cisco 12 S",
"5": "Cisco 30 VIP",
"6": "Cisco 7910",
"7": "Cisco 7960",
"8": "Cisco 7940",
"9": "Cisco 7935",
"10": "Cisco VGC Phone",
"11": "Cisco VGC Virtual Phone",
"12": "Cisco ATA 186",
"15": "EMCC Base Phone",
"20": "SCCP Phone",
"30": "Analog Access",
"40": "Digital Access",
"42": "Digital Access+",
"43": "Digital Access WS-X6608",
"47": "Analog Access WS-X6624",
"48": "VGC Gateway",
"50": "Conference Bridge",
"51": "Conference Bridge WS-X6608",
"52": "Cisco IOS Conference Bridge (HDV2)",
"53": "Cisco Conference Bridge (WS-SVC-CMM)",
"61": "H.323 Phone",
"62": "H.323 Gateway",
"70": "Music On Hold",
"71": "Device Pilot",
"72": "CTI Port",
"73": "CTI Route Point",
"80": "Voice Mail Port",
"83": "Cisco IOS Software Media Termination Point (HDV2)",
"84": "Cisco Media Server (WS-SVC-CMM-MS)",
"85": "Cisco Video Conference Bridge (IPVC-35xx)",
"86": "Cisco IOS Heterogeneous Video Conference Bridge",
"87": "Cisco IOS Guaranteed Audio Video Conference Bridge",
"88": "Cisco IOS Homogeneous Video Conference Bridge",
"90": "Route List",
"100": "Load Simulator",
"110": "Media Termination Point",
"111": "Media Termination Point Hardware",
"112": "Cisco IOS Media Termination Point (HDV2)",
"113": "Cisco Media Termination Point (WS-SVC-CMM)",
"115": "Cisco 7941",
"119": "Cisco 7971",
"120": "MGCP Station",
"121": "MGCP Trunk",
"122": "GateKeeper",
"124": "7914 14-Button Line Expansion Module",
"125": "Trunk",
"126": "Tone Announcement Player",
"131": "SIP Trunk",
"132": "SIP Gateway",
"133": "WSM Trunk",
"134": "Remote Destination Profile",
"227": "7915 12-Button Line Expansion Module",
"228": "7915 24-Button Line Expansion Module",
"229": "7916 12-Button Line Expansion Module",
"230": "7916 24-Button Line Expansion Module",
"232": "CKEM 36-Button Line Expansion Module",
"253": "SPA8800",
"254": "Unknown MGCP Gateway",
"255": "Unknown",
"302": "Cisco 7985",
"307": "Cisco 7911",
"308": "Cisco 7961G-GE",
"309": "Cisco 7941G-GE",
"335": "Motorola CN622",
"336": "Third-party SIP Device (Basic)",
"348": "Cisco 7931",
"358": "Cisco Unified Personal Communicator",
"365": "Cisco 7921",
"369": "Cisco 7906",
"374": "Third-party SIP Device (Advanced)",
"375": "Cisco TelePresence",
"376": "Nokia S60",
"404": "Cisco 7962",
"412": "Cisco 3951",
"431": "Cisco 7937",
"434": "Cisco 7942",
"435": "Cisco 7945",
"436": "Cisco 7965",
"437": "Cisco 7975",
"446": "Cisco 3911",
"468": "Cisco Unified Mobile Communicator",
"478": "Cisco TelePresence 1000",
"479": "Cisco TelePresence 3000",
"480": "Cisco TelePresence 3200",
"481": "Cisco TelePresence 500-37",
"484": "Cisco 7925",
"486": "Syn-Apps Virtual Phone",
"493": "Cisco 9971",
"495": "Cisco 6921",
"496": "Cisco 6941",
"497": "Cisco 6961",
"503": "Cisco Unified Client Services Framework",
"505": "Cisco TelePresence 1300-65",
"520": "Cisco TelePresence 1100",
"521": "Transnova S3",
"522": "BlackBerry MVS VoWifi",
"527": "IPTrade TAD",
"537": "Cisco 9951",
"540": "Cisco 8961",
"547": "Cisco 6901",
"548": "Cisco 6911",
"550": "Cisco ATA 187",
"557": "Cisco TelePresence 200",
"558": "Cisco TelePresence 400",
"562": "Cisco Dual Mode for iPhone",
"564": "Cisco 6945",
"575": "Cisco Dual Mode for Android",
"577": "Cisco 7926",
"580": "Cisco E20",
"582": "Generic Single Screen Room System",
"583": "Generic Multiple Screen Room System",
"584": "Cisco TelePresence EX90",
"585": "Cisco 8945",
"586": "Cisco 8941",
"588": "Generic Desktop Video Endpoint",
"590": "Cisco TelePresence 500-32",
"591": "Cisco TelePresence 1300-47",
"592": "Cisco 3905",
"593": "Cisco Cius",
"594": "VKEM 36-Button Line Expansion Module",
"596": "Cisco TelePresence TX1310-65",
"597": "Cisco TelePresence MCU",
"598": "Ascom IP-DECT Device",
"599": "Cisco TelePresence Exchange System",
"604": "Cisco TelePresence EX60",
"606": "Cisco TelePresence Codec C90",
"607": "Cisco TelePresence Codec C60",
"608": "Cisco TelePresence Codec C40",
"609": "Cisco TelePresence Quick Set C20",
"610": "Cisco TelePresence Profile 42 (C20)",
"611": "Cisco TelePresence Profile 42 (C60)",
"612": "Cisco TelePresence Profile 52 (C40)",
"613": "Cisco TelePresence Profile 52 (C60)",
"614": "Cisco TelePresence Profile 52 Dual (C60)",
"615": "Cisco TelePresence Profile 65 (C60)",
"616": "Cisco TelePresence Profile 65 Dual (C90)",
"617": "Cisco TelePresence MX200",
"619": "Cisco TelePresence TX9000",
"621": "Cisco 7821",
"620": "Cisco TelePresence TX9200",
"622": "Cisco 7841",
"623": "Cisco 7861",
"626": "Cisco TelePresence SX20",
"627": "Cisco TelePresence MX300",
"628": "IMS-integrated Mobile (Basic)",
"631": "Third-party AS-SIP Endpoint",
"632": "Cisco Cius SP",
"633": "Cisco TelePresence Profile 42 (C40)",
"634": "Cisco VXC 6215",
"635": "CTI Remote Device",
"640": "Usage Profile",
"642": "Carrier-integrated Mobile",
"645": "Universal Device Template",
"647": "Cisco DX650",
"648": "Cisco Unified Communications for RTX",
"652": "Cisco Jabber for Tablet",
"659": "Cisco 8831",
"682": "Cisco TelePresence SX10",
"683": "Cisco 8841",
"684": "Cisco 8851",
"685": "Cisco 8861",
"688": "Cisco TelePresence SX80",
"689": "Cisco TelePresence MX200 G2",
"690": "Cisco TelePresence MX300 G2",
"20000": "Cisco 7905",
"30002": "Cisco 7920",
"30006": "Cisco 7970",
"30007": "Cisco 7912",
"30008": "Cisco 7902",
"30016": "Cisco IP Communicator",
"30018": "Cisco 7961",
"30019": "Cisco 7936",
"30027": "Analog Phone",
"30028": "ISDN BRI Phone",
"30032": "SCCP gateway virtual phone",
"30035": "IP-STE",
"36041": "Cisco TelePresence Conductor",
"36042": "Cisco DX80",
"36043": "Cisco DX70",
"36049": "BEKEM 36-Button Line Expansion Module",
"36207": "Cisco TelePresence MX700",
"36208": "Cisco TelePresence MX800",
}
# Given CUCM XML, use XPath to extract relevant details for each device
# searching based on device type. Return a list of devices' information.
def get_device_details(xml, device):
namespace = {"ns1": "http://schemas.cisco.com/ast/soap"}
items = xml.findall(f".//ns1:DeviceClass[.='{device}']/..", namespace)
names_seen = {}
trunk_details = []
for item in items:
ip = item.find(".//ns1:IP", namespace).text
name = item.find("ns1:Name", namespace).text
model = item.find("ns1:Model", namespace).text
status = item.find("ns1:Status", namespace).text
reason = item.find("ns1:StatusReason", namespace).text
if not names_seen.get(name):
trunk_details.append({
"ip": ip,
"name": name,
"status": status,
"status_reason": status_reason_lookup.get(reason),
"type": device,
"model_name": model_name_lookup.get(model) or "Unknown"
})
names_seen[name] = True
return trunk_details
# Contact CUCM and query it for device information for the following device
# types: SIP trunks, hunt lists, H323 and media resources (e.g. IVR). Return
# a list for devices' information.
def get_devices(addr, port, user, password, insecure):
devices = []
for device in ["SIPTrunk", "MediaResources", "H323", "HuntList"]:
cucm_xml = query_cucm(addr, port, user, password, insecure, device)
details = get_device_details(cucm_xml, device)
devices.extend(details)
return devices
# Parse args, contact CUCM, check status of non-hone devices, and then print
# results
def main(argv=None):
if argv is None:
argv = sys.argv[1:]
args = inv.parse_arguments(argv)
devices = get_devices(args.hostname, args.port, args.user, args.password,
args.insecure)
inv.print_out(devices, "cucm_chk")
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,434 @@
#!/usr/bin/env python3
#
# Given a CUCM host, query the CUCM to get a complete list of phones, then
# query all those phones concurrently for additional details, then print
# the results out in a format CheckMK understands.
#
# Run the command on the console for a complete list of options.
#
# This script is designed to work with up to 90K phones, and ideally below 10K.
# If more than 10K phones are queried, this script should be modified to
# perform connection reuse to improve performance. Beyond 90K support for
# paginating the AXL API must be added.
phone_query_timeout = 10 # max time to query a single phone
phone_queries_timeout = 45 # max time to query all phones
cucm_page_size = 1000 # CUCM will not return pages larger than 2000.
# Larger page sizes cause notably longer queries,
# so a default of 1000 devices per query is a
# safer number.
import urllib.request, base64, sys, argparse, asyncio, re, json, ssl, html
from xml.etree import ElementTree
from textwrap import wrap
# Create a TLS context for client connections.
def create_ssl_ctx():
ctx = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT)
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return ctx
# Typical GET or POST HTTP with Basic auth (using user and password
# credientials). Returns data structure parsed from XML.
def get_url(url, user, password, insecure, headers, data):
request = urllib.request.Request(url, data=bytes(data, 'ascii'))
for header, value in headers:
request.add_header(header, value)
if user and password:
auth_str = base64.b64encode(bytes('%s:%s' % (user, password), 'ascii'))
request.add_header('Authorization', 'Basic %s' % auth_str.decode('utf-8'))
ctx = None
if insecure:
ctx = create_ssl_ctx()
with urllib.request.urlopen(request, context=ctx) as conn:
xml_data = conn.read()
return ElementTree.fromstring(xml_data)
# Call the CUCM AXL API synchronously, using a SOAP query to fetch the names of
# all phones. It returns XML, which we parse. We call the AXL API because the
# RisPort70 API (see query_cucm_risport() below) does not support pagination,
# so we need to get a full list of phone names from AXL first, then do multiple
# queries on RisPort70 using subsets of the phone name list found from AXL.
#
# The AXL API has a return limit of 8MB, which is around 90K phones, so we
# don't bother paginating the AXL API itself; if more than that is needed, add
# pagination here.
#
# References:
# https://developer.cisco.com/docs/axl/
# https://github.com/reillychase/How-to-return-Cisco-RIS-with-more-than-1000-results/blob/master/main.py
def query_cucm_axl(addr, port, user, password, insecure):
url = 'https://%s:%s/axl/' % (addr, port)
headers = [
('Content-Type', 'text/xml'),
('Accept', 'text/xml'),
('SOAPAction', 'CUCM:DB ver=12.5'),
]
try:
return get_url(url, user, password, insecure, headers, """
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns="http://www.cisco.com/AXL/API/12.5">
<soapenv:Header/>
<soapenv:Body>
<ns:listPhone>
<searchCriteria>
<name>%</name>
</searchCriteria>
<returnedTags>
<name/>
</returnedTags>
</ns:listPhone>
</soapenv:Body>
</soapenv:Envelope>
""")
except urllib.error.HTTPError as e:
sys.stderr.write("AXL error: %s\n" % e)
# Call the CUCM RisPort70 API synchronously, using a SOAP query to fetch
# information about the phones with ids listed in the phone_ids arg. It returns
# XML, which we parse.
#
# Be aware that the API will return information about a maximum of 2000 devices,
# and provides no means of pagination. In order to do pagination, we first need
# to query the AXL API for a list of phone names, then all this function
# repeatedly with a different subset of 2000 phones from that complete list.
#
# Although this function will allow pages for 2000 devices, it's recommended to
# use less for each call to avoid timeouts. The default maximum size of
# phone_ids is 1000, although this can be varied by changing the cucm_page_size
# at the top of this file.
#
# References:
# https://developer.cisco.com/docs/sxml/#!risport70-api-reference
# https://paultursan.com/2018/12/getting-cucm-real-time-data-via-risport70-with-python-and-zeep-cisco-serviceability-api/
def query_cucm_risport(addr, port, user, password, insecure, phone_ids):
assert len(phone_ids) <= 2000
url = 'https://%s:%s/realtimeservice2/services/RISService70/' % (addr, port)
headers = [('Content-Type', 'text/plain')]
id_query = ''.join([f'<soap:item><soap:Item>{id}</soap:Item></soap:item>' for id in phone_ids])
try:
return get_url(url, user, password, insecure, headers, f"""
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:soap="http://schemas.cisco.com/ast/soap">
<soapenv:Header/>
<soapenv:Body>
<soap:selectCmDevice>
<soap:StateInfo></soap:StateInfo>
<soap:CmSelectionCriteria>
<soap:MaxReturnedDevices>2000</soap:MaxReturnedDevices>
<soap:DeviceClass>Any</soap:DeviceClass>
<soap:Model>255</soap:Model>
<soap:Status>Registered</soap:Status>
<soap:NodeName></soap:NodeName>
<soap:SelectBy>Name</soap:SelectBy>
<soap:SelectItems>
{id_query}
</soap:SelectItems>
<soap:Protocol>Any</soap:Protocol>
<soap:DownloadStatus>Any</soap:DownloadStatus>
</soap:CmSelectionCriteria>
</soap:selectCmDevice>
</soapenv:Body>
</soapenv:Envelope>
""")
except urllib.error.HTTPError as e:
sys.stderr.write("CUCM error: %s\n" % e)
# Given AXL XML, use XPath to extract names for all phones.
def get_phone_ids(xml):
# should this be ns2?
namespace = {'ns': 'http://www.cisco.com/AXL/API/12.5'}
items = xml.findall(".//phone/name", namespace)
names = []
for item in items:
names.append(item.text)
return names
# Given CUCM XML, use XPath to extract a bunch of details for each phone.
def get_phone_details(xml):
namespace = {'ns1': 'http://schemas.cisco.com/ast/soap'}
items = xml.findall(".//ns1:DeviceClass[.='Phone']/..", namespace)
names_seen = {}
phone_details = []
for item in items:
ip = item.find('.//ns1:IP', namespace).text
name = item.find('ns1:Name', namespace).text
dir_num = item.find('ns1:DirNumber', namespace).text
description = item.find('ns1:Description', namespace).text
user = item.find('ns1:LoginUserId', namespace).text
# These come with a -Registered on the end of the numbers.
# Since all numbers we get from CUCM are registered, there's no
# need for the -Registered, and we cut it off here.
if dir_num:
dir_num = dir_num.split('-')[0]
if not names_seen.get(name):
phone_details.append((name, ip, dir_num, user, description))
names_seen[name] = True
return phone_details
# If a phone (possibly) returns XML, attempt to extract the MAC, serial and
# model. We're using regex here, instead of full-blown XML parsing, to minimize
# the time and GC garbage generated.
def get_phone_details_from_xml(data):
# if the HTTP server didn't return a 200...
if data.find('200 OK') == -1:
return None, None, None
# attempt to extract info from XML
mac = re.search('<MACAddress>(.+)</MACAddress>', data)
serial = re.search('<serialNumber>(.+)</serialNumber>', data)
model = re.search('<modelNumber>(.+)</modelNumber>', data)
mac_str = mac and normalize_mac(html.unescape(mac[1]))
ser_str = serial and html.unescape(serial[1])
mod_str = model and html.unescape(model[1])
return mac_str, ser_str, mod_str
# If a phone (possibly) returns HTML, attempt to extract the MAC, serial and
# model. We use regex here for the same reason we use it in
# get_phone_details_from_xml().
def get_phone_details_from_html(data):
if data.find('200 OK') == -1:
return None, None, None
mac = None
serial = None
model = None
# attempt to extract info from HTML
matches = re.findall('<b>\s*(.*?)\s*</b>', data, re.M | re.I)
for i, txt in enumerate(matches):
txt = txt.lower()
if not mac and txt == 'mac address':
mac = normalize_mac(html.unescape(matches[i + 1]))
elif not serial and txt == 'serial number':
serial = html.unescape(matches[i + 1])
elif not model and txt == 'model number':
model = html.unescape(matches[i + 1])
elif mac and serial and model:
break
return mac, serial, model
# Different phones return MACs in different formats. We convert them to a single
# canonical format here.
def normalize_mac(mac):
if mac.find(":") != -1:
return mac.lower()
else:
return ":".join(wrap(mac, 2)).lower()
# Create a new HTTP/HTTPS connection, send a request, and extract any results.
async def get_async_url(ip, url, insecure=False):
ctx = None
port = 80
if not insecure:
ctx = create_ssl_ctx()
port = 443
# XXX switch from 127.0.0.1:8081 to ip:port
# future = asyncio.open_connection('127.0.0.1', 8081, ssl=ctx)
future = asyncio.open_connection(ip, port, ssl=ctx)
reader, writer = await asyncio.wait_for(future, timeout=phone_query_timeout)
query = f'GET {url} HTTP/1.1\r\nHost: {ip}\r\nConnection: close\r\n\r\n'
writer.write(query.encode())
await writer.drain()
data = ''
while not reader.at_eof():
raw = await reader.read(-1)
data += raw.decode()
writer.close()
await writer.wait_closed()
return data
# Asynchronously contact the HTTP server in a phone. There are several
# different URLs that might return information, depending on the model of phone.
# To fetch the MAC and serial details we want requires us to potentially call
# all endpoints until we get some results. Be aware that some phone HTTP servers
# return 200 (and empty results) if we call the wrong URL for that model.
#
# We attempt to contact the phone using HTTPS first, falling back to HTTP if
# attempts with HTTPS failed.
#
# Originally we tried to take advantage of HTTP connection reuse, but this was
# causing some problems, and it wasn't worth the effort to handle the edge
# cases. Now we always make a new connection per request, even when it's
# multiple URLs on the same IP. If additional performance is ever needed, HTTP
# connection reuse is worth adding; depending on the latency it can easily
# 2x+ request rate.
async def query_phone_info_now(details):
name, ip, dir_num, user, description = details
mac = None
serial = None
model = None
try:
data = await get_async_url(ip, '/DeviceInformationX')
mac, serial, model = get_phone_details_from_xml(data)
if not mac:
data = await get_async_url(ip, '/Device_Information.html')
mac, serial, model = get_phone_details_from_html(data)
if not mac:
data = await get_async_url(ip, '/CGI/Java/Serviceability?adapter=device.statistics.device')
mac, serial, model = get_phone_details_from_html(data)
if not mac:
data = await get_async_url(ip, '/')
mac, serial, model = get_phone_details_from_html(data)
except (ConnectionRefusedError, asyncio.TimeoutError, ssl.SSLError):
try:
if not mac:
data = await get_async_url(ip, '/DeviceInformationX', True)
mac, serial, model = get_phone_details_from_xml(data)
if not mac:
data = await get_async_url(ip, '/Device_Information.html', True)
mac, serial, model = get_phone_details_from_html(data)
if not mac:
data = await get_async_url(ip, '/CGI/Java/Serviceability?adapter=device.statistics.device', True)
mac, serial, model = get_phone_details_from_html(data)
if not mac:
data = await get_async_url(ip, '/', True)
mac, serial, model = get_phone_details_from_html(data)
except (ConnectionRefusedError, asyncio.TimeoutError):
pass
return {
"name": name,
"ip": ip,
"mac": mac,
"serial": serial,
"dir_num": dir_num,
"model": model,
"user": user,
"description": description
}
# This functions job is solely to keep a limit on the concurrent number of
# connections made to the phones. Without this limit we'd quickly run out of
# spare sockets when dealing with large numbers of phones.
async def query_phone_info(details, semaphore):
async with semaphore:
return await query_phone_info_now(details)
# Given information about a list of phones (specifically, their IP addresses),
# we call the HTTP server on each phone to extract the MAC and serial. We
# return with a list of dicts containing information about all phones.
# Contacting thousands of phones serially would take too long, so we keep 200
# concurrent calls in-flight to the phones to shorten all querying to a few
# seconds.
async def query_phones(details):
sem = asyncio.Semaphore(200)
tasks = map(lambda d: asyncio.create_task(query_phone_info(d, sem)), details)
done, pending = await asyncio.wait(tasks, timeout=phone_queries_timeout)
# we silently ignore pending for now
return map(lambda f: f.result(), done)
# Given an array of phone names, do paginated queries to the CUCM for phone
# information, then asynchronously query all the phones. While the CUCM has
# most of the information we want about a phone, it critically lacks the serial
# and MAC of the phone, which is why we need to fetch the details from the
# phone itself over an HTTP server each phone has.
def get_phones(addr, port, user, password, insecure):
axl_xml = query_cucm_axl(addr, port, user, password, insecure)
phone_ids = get_phone_ids(axl_xml)
phone_details = []
page_size = cucm_page_size
for i in range(0, len(phone_ids), page_size):
ids = phone_ids[i:i + page_size]
cucm_xml = query_cucm_risport(addr, port, user, password, insecure, ids)
details = get_phone_details(cucm_xml)
phone_details.extend(details)
return asyncio.run(query_phones(phone_details))
# Print out all our results in a format that CheckMK understands. Most of our
# output are in JSON rows.
def print_out(device_info, agent_name):
sys.stdout.write(f"<<<{agent_name}:sep(0)>>>\n")
device_info = list(device_info)
device_info.sort(key=lambda d: d["ip"])
for entry in device_info:
sys.stdout.write("%s\n" % json.dumps(entry))
# Parse the command-line arguments. We have several options, but hostname is
# always required. Print out help to console if we get no args.
def parse_arguments(argv):
parser = argparse.ArgumentParser()
parser.add_argument(
"-u", "--user", default=None, help="Username for CUCM login"
)
parser.add_argument(
"-s", "--password", default=None, help="Password for CUCM login"
)
parser.add_argument(
"-p", "--port", default=443, type=int, help="Use alternative port (default: 443)"
)
parser.add_argument(
"hostname", metavar="HOSTNAME", help="Hostname of the CUCM to query."
)
parser.add_argument(
"-k", "--insecure", default=False, help="Skip certificate verification",
action="store_true"
)
return parser.parse_args(argv)
# Parse args, contact CUCM, query phones, and then print results
def main(argv=None):
if argv is None:
argv = sys.argv[1:]
args = parse_arguments(argv)
phones = get_phones(args.hostname, args.port, args.user, args.password,
args.insecure)
print_out(phones, 'cucm_inv')
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
def agent_cucm_arguments(params, hostname, ipaddress):
args = []
if "user" in params:
args += ["-u", params["user"]]
if "password" in params:
args += ["-s", params["password"]]
if "port" in params:
args += ["-p", params["port"]]
if params.get("insecure"):
args.append("-k")
args.append(params["instance"])
return args
special_agent_info["cucm_chk"] = agent_cucm_arguments
special_agent_info["cucm_inv"] = agent_cucm_arguments

View File

@ -0,0 +1,19 @@
from cmk.gui.plugins.views import inventory_displayhints
from cmk.gui.i18n import _l
# inventory list for phones found in CUCM
inventory_displayhints.update({
".phones:": {
"title": _l("Phones"),
"keyorder": ["name", "user", "dir_num", "ip", "model", "serial", "mac", "description"],
},
".phones:*.name": {"title": _l("Name")},
".phones:*.user": {"title": _l("User")},
".phones:*.dir_num": {"title": _l("Dir Num")},
".phones:*.ip": {"title": _l("IP Addr")},
".phones:*.model": {"title": _l("Model")},
".phones:*.serial": {"title": _l("Serial")},
".phones:*.mac": {"title": _l("MAC Addr")},
".phones:*.description": {"title": _l("Description")},
})

View File

@ -0,0 +1,120 @@
#!/usr/bin/env python3
#
# GUI configuration pages to set up inventorying and checks done by the CUCM
# agent to CUCM. These two pages are for giving the agent the necessary details
# to connect to CUCM (e.g. IP address, user, login, etc).
#
# Ideally, we'd have a single page to configure both the inventorying and
# checks, since both contact the same CUCM instance. Unfortunately, I didn't
# find a clean way to do it, so we're left with two identical GUI pages that
# take identical information. At least we manage to share most of the code
# here by taking a deep copy and modifying the title.
import copy
from cmk.gui.i18n import _
from cmk.gui.plugins.wato.utils import (
rulespec_registry,
HostRulespec,
RulespecGroupCheckParametersHardware
)
from cmk.gui.plugins.wato.inventory import RulespecGroupInventory
from cmk.gui.watolib.rulespecs import Rulespec
from cmk.gui.valuespec import (
Dictionary,
TextInput,
Hostname,
NetworkPort,
Password,
TextAscii,
FixedValue
)
# GUI config page for inventory.
def _valuespec_special_agents_cucm_inv():
return Dictionary(
title=_("CUCM inventory"),
help=_(""),
optional_keys=["port", "user", "password", "insecure"],
elements=[
(
"instance",
Hostname(
title=_("Hostname"),
help=_(
"Host of CUCM host for query"
),
allow_empty=False,
),
),
(
"port",
NetworkPort(
title=_("Port"),
help=_(
"Port of CUCM host for query"
),
minvalue=1,
default_value=443,
),
),
(
"user",
TextInput(
title=_("Username"),
help=_(
"Username used when querying CUCM"
),
),
),
(
"password",
Password(
title=_("Password"),
help=_(
"Password used when querying CUCM"
),
),
),
(
"insecure",
FixedValue(
True,
title=_("Insecure"),
totext=_("Disable SSL certificate verification"),
help=_(
"Ignore unverified HTTPS request warnings when contacting CUCM"
),
),
),
],
)
# GUI config page for checks. We do a deep copy of the above function and just
# change the title. A bit hackish since we're changing a private attribute.
def _valuespec_special_agents_cucm_chk():
inv_spec = _valuespec_special_agents_cucm_inv()
chk_spec = copy.deepcopy(inv_spec)
chk_spec._title=_("CUCM checks")
return chk_spec
rulespec_registry.register(
HostRulespec(
factory_default=Rulespec.FACTORY_DEFAULT_UNUSED,
name="special_agents:cucm_inv",
group=RulespecGroupInventory,
valuespec=_valuespec_special_agents_cucm_inv,
)
)
rulespec_registry.register(
HostRulespec(
factory_default=Rulespec.FACTORY_DEFAULT_UNUSED,
name="special_agents:cucm_chk",
group=RulespecGroupCheckParametersHardware,
valuespec=_valuespec_special_agents_cucm_chk,
)
)

BIN
domains/domain_checks-0.2.0.mkp Executable file

Binary file not shown.

View File

@ -0,0 +1,64 @@
#!/usr/bin/env python3
# Copyright (C) 2025 Spearhead Systems SRL
import datetime
import json
from cmk.agent_based.v2 import Result, Service, State, CheckPlugin, AgentSection
# Incoming agent output is JSON of the form:
#
# {"domain": "google.com", "state": "CRIT", "expires": "2028-09-14"}
#
# "domain" and "state" are always present, whereas "expires" is optional
# (e.g. most ccTLDs do not return expiry information). The "state" is one of
# (OK, WARN, CRIT, UNKNOWN).
#
# Return a dictionary which uses domain as key, and the rest of the dictionary
# as a value.
def parse_domains_expiry(string_table):
results = {}
for [line] in string_table:
result = json.loads(line)
results[result["domain"]] = result
return results
def discover_domains_expiry(section):
for domain in section.keys():
yield Service(item=domain)
def check_domains_expiry(item, params, section):
data = section.get(item)
if not data:
yield Result(state=State.WARN, summary="Expiry not found in whois")
return
state = State[data["state"]]
expiry = data.get("expires")
if expiry:
summary = f"Expires on {expiry}"
else:
summary = "No expiry found"
yield Result(state=state, summary=summary)
agent_section_domains_expiry = AgentSection(
name="domains_expiry",
parse_function=parse_domains_expiry
)
check_plugin_domains_expiry = CheckPlugin(
name="domains_expiry",
service_name="Expiry for Domain '%s'",
discovery_function=discover_domains_expiry,
check_function=check_domains_expiry,
check_default_parameters={},
)

View File

@ -0,0 +1,72 @@
#!/usr/bin/env python3
# Copyright (C) 2025 Spearhead Systems SRL
import json
from cmk.agent_based.v2 import Result, Service, State, CheckPlugin, AgentSection
# Incoming agent output is JSON of the form:
#
# {"domain": "yahoo.com", "state": "WARN", "unexpected": ["ns5.yahoo.com"], "missing": ["ns6.yahoo.com"]}
#
# Each JSON row is for a single domain. Each row always has "domain" and "state"
# fields, with optional "unexpected" and "missing" fields. The "state" field is
# one of "OK", "WARN", "CRIT", or "UNKNOWN". The "unexpected" field is a list of
# unwanted nameservers that are present in the dig query result, whereas the
# "missing" field are wanted nameservers that are not present.
#
# Return a dictionary which uses domain as key, and a dictionary of
# state/unexpected/missing.
def parse_domains_nameservers(string_table):
results = {}
for [line] in string_table:
result = json.loads(line)
results[result["domain"]] = result
return results
def discover_domains_nameservers(section):
for domain in section.keys():
yield Service(item=domain)
def check_domains_nameservers(item, params, section):
data = section.get(item)
if not data:
yield Result(state=State.WARN, summary="Nameservers missing")
return
state = State[data["state"]]
unexpected = data.get("unexpected")
missing = data.get("missing")
summary = ""
if unexpected:
summary += f"Unexpected nameserver(s) found: {''.join(unexpected)}. "
if missing:
summary += f"Expected nameserver(s) missing: {''.join(missing)}. "
if not summary:
summary = "All expected nameservers found, none unexpected found"
yield Result(state=state, summary=summary)
agent_section_triton_wedge = AgentSection(
name="domains_nameservers",
parse_function=parse_domains_nameservers
)
check_plugin_domains_nameservers = CheckPlugin(
name="domains_nameservers",
service_name="Nameservers for Domain '%s'",
discovery_function=discover_domains_nameservers,
check_function=check_domains_nameservers,
check_default_parameters={},
)

View File

@ -0,0 +1,42 @@
#!/bin/bash
# Copyright (C) 2025 Spearhead Systems SRL
set -eu
if [[ $# < 3 ]]; then
echo "Usage: ${@: 0:1} <domains> <crit date> <warn date>" 1>&2
echo "Example: ${@: 0:1} google.com yahoo.com 2024-05-07 2024-05-21" 1>&2
exit 1
fi
# Extract from args
domains="${@: 1:$#-2}"
warn="${@: -1:1}"
crit="${@: -2:1}"
echo "<<<domains_expiry:sep(0)>>>"
for domain in $domains; do
echo -n "{\"domain\": \"$domain\", \"state\": \""
# Unfortunately, there's no actual format for whois entries, so this is a
# best-effort based on things seen in the wild. Note that ccTLDs usually
# do not publish expiry dates at all.
expires=$(whois "$domain" | grep 'Expir.*' | head -1 | grep -Eo '[0-9]{4}-[0-9]{2}-[0-9]{2}' || true)
if [[ "$expires" == "" ]]; then
echo -n "UNKNOWN"
elif [[ "$expires" < "$crit" ]]; then
echo -n "CRIT"
elif [[ "$expires" < "$warn" ]]; then
echo -n "WARN"
else
echo -n "OK"
fi
if [[ "$expires" == "" ]]; then
echo "\"}"
else
echo "\", \"expires\": \"$expires\"}"
fi
done

View File

@ -0,0 +1,47 @@
#!/usr/bin/env python3
# Copyright (C) 2025 Spearhead Systems SRL
import json
import optparse
import subprocess
print("<<<domains_nameservers:sep(0)>>>")
parser = optparse.OptionParser()
parser.add_option('-d', '--domains', action="append")
parser.add_option('-n', '--nameservers', action="append")
parser.add_option('-a', '--alert', default="WARN")
opts, _ = parser.parse_args()
alert = opts.alert
assert ["OK", "WARN", "CRIT", "UNKNOWN"].count(alert)
for domain_str, nameserver_str in zip(opts.domains, opts.nameservers):
domains = domain_str.split(",")
nameservers = set(nameserver_str.split(","))
for domain in domains:
dig_result = subprocess.run(["dig", "+short", "NS", domain], capture_output=True)
dig_ns = set(dig_result.stdout.decode("utf-8").split('.\n'))
dig_ns.remove('')
result = {
"domain": domain,
"state": "OK",
}
if dig_ns != nameservers:
result["state"] = alert
unexpected = list(dig_ns - nameservers)
missing = list(nameservers - dig_ns)
if unexpected:
result["unexpected"] = unexpected
if missing:
result["missing"] = missing
print(json.dumps(result))

View File

@ -0,0 +1,52 @@
#!/usr/bin/env python3
# Copyright (C) 2025 Spearhead Systems SRL
from cmk.rulesets.v1.form_specs.validators import LengthInRange, NumberInRange
from cmk.rulesets.v1.form_specs import Dictionary, DictElement, List, String, Integer, DefaultValue
from cmk.rulesets.v1.rule_specs import SpecialAgent, Topic, Title, Help
def _formspec():
return Dictionary(
title=Title("Domains Expiry"),
elements={
"domains": DictElement(
required=True,
parameter_form=List(
title=Title("Domain names"),
help_text=Help("List of domain names to check"),
editable_order=False,
custom_validate=(LengthInRange(min_value=1),),
element_template=String(
custom_validate=(LengthInRange(min_value=3),),
),
),
),
"days_warn": DictElement(
required=True,
parameter_form=Integer(
title=Title("Warn if expires within days"),
help_text=Help("If there are fewer days until one of the above domains expires, issue an alert"),
custom_validate=(NumberInRange(min_value=0),),
prefill=DefaultValue(30),
),
),
"days_crit": DictElement(
required=True,
parameter_form=Integer(
title=Title("Crit if expires within days"),
help_text=Help("If there are fewer days until one of the above domains expires, issue an alert"),
custom_validate=(NumberInRange(min_value=0),),
prefill=DefaultValue(7),
),
),
}
)
rule_spec_agent_config_domains_expiry = SpecialAgent(
topic=Topic.NETWORKING,
name="domains_expiry",
title=Title("Domains Expiry"),
parameter_form=_formspec,
)

View File

@ -0,0 +1,70 @@
#!/usr/bin/env python3
# Copyright (C) 2025 Spearhead Systems SRL
from cmk.rulesets.v1.form_specs.validators import LengthInRange
from cmk.rulesets.v1.form_specs import Dictionary, DictElement, SingleChoice, SingleChoiceElement, List, String, DefaultValue
from cmk.rulesets.v1.rule_specs import SpecialAgent, Topic, Title, Help
from cmk.agent_based.v2 import State
def _formspec():
return Dictionary(
title=Title("Domains Nameservers"),
elements={
"domain_nameservers": DictElement(
required=True,
parameter_form=List(
title=Title("Domains to check"),
help_text=Help("List of domain names to check"),
editable_order=False,
custom_validate=(LengthInRange(min_value=1),),
element_template=Dictionary(
elements={
"domains": DictElement(
required=True,
parameter_form=List(
title=Title("Domain names"),
help_text=Help("List of domain names the below nameservers apply to"),
custom_validate=(LengthInRange(min_value=1),),
element_template=String(
custom_validate=(LengthInRange(min_value=3),),
),
),
),
"nameservers": DictElement(
required=True,
parameter_form=List(
title=Title("Nameservers"),
help_text=Help("List of nameservers that the above domain names should have"),
custom_validate=(LengthInRange(min_value=1),),
element_template=String(
custom_validate=(LengthInRange(min_value=3),),
),
),
),
}
)
),
),
"alert_level": DictElement(
required=True,
parameter_form=SingleChoice(
title=Title("Alert level used on mismatch"),
help_text=Help("Alert level used when there is a mismatch in domain name servers for a domain"),
prefill=DefaultValue(State.WARN.name),
elements=[
SingleChoiceElement(name=State.CRIT.name, title=Title(State.CRIT.name)),
SingleChoiceElement(name=State.WARN.name, title=Title(State.WARN.name)),
SingleChoiceElement(name=State.OK.name, title=Title(State.OK.name)),
],
),
),
},
)
rule_spec_agent_config_domains_nameservers = SpecialAgent(
topic=Topic.NETWORKING,
name="domains_nameservers",
title=Title("Domains Nameservers"),
parameter_form=_formspec,
)

View File

@ -0,0 +1,23 @@
#!/usr/bin/env python3
# Copyright (C) 2025 Spearhead Systems SRL
from datetime import datetime, timedelta
from cmk.server_side_calls.v1 import noop_parser, SpecialAgentConfig, SpecialAgentCommand
def _agent_arguments(params, host_config):
today = datetime.today()
args = params["domains"]
for key in ["days_crit", "days_warn"]:
date = (today + timedelta(days=params[key])).date()
args.append(str(date))
yield SpecialAgentCommand(command_arguments=args)
special_agent_domains_expiry = SpecialAgentConfig(
name="domains_expiry",
parameter_parser=noop_parser,
commands_function=_agent_arguments,
)

View File

@ -0,0 +1,34 @@
#!/usr/bin/env python3
# Copyright (C) 2025 Spearhead Systems SRL
from cmk.server_side_calls.v1 import noop_parser, SpecialAgentConfig, SpecialAgentCommand
# Although the API makes it seem like we can yield several
# SpecialAgentCommands, the use of a table for _elems in
# lib/python3/cmk/base/sources/_builder.py (at least in 2.3.0p31) means that
# we can only emit one SpecialAgentCommands. This CheckMK bug means that
# SpecialAgentCommands() will be forgotten.
#
# Alas, that means we need to pack everything into a single command invocation.
def _agent_arguments(params, host_config):
args = []
alert_level = params["alert_level"]
for ele in params["domain_nameservers"]:
domains = ele["domains"]
nameservers = ele["nameservers"]
args.append("--domains=" + ",".join(domains))
args.append("--nameservers=" + ",".join(nameservers))
args.append("--alert=" + alert_level)
yield SpecialAgentCommand(command_arguments=args)
special_agent_domains_nameservers = SpecialAgentConfig(
name="domains_nameservers",
parameter_parser=noop_parser,
commands_function=_agent_arguments,
)

Some files were not shown because too many files have changed in this diff Show More