From 99ef4d6aeed5a96393d4fcd6bc66f20de0b17fce Mon Sep 17 00:00:00 2001 From: "th.l" <thl-cmk@outlook.com> Date: Sun, 30 Jun 2024 18:42:23 +0200 Subject: [PATCH] update project --- README.md | 2 +- mkp/cisco_meraki-1.3.2-20240660.mkp | Bin 0 -> 41892 bytes .../cisco_meraki_org_device_info.py | 4 +- .../cisco_meraki_org_device_status.py | 6 +- .../cisco_meraki_org_licenses_overview.py | 55 ++- .../cisco_meraki_org_sensor_readings.py_ | 240 +++++++++++++ source/checks/agent_cisco_meraki | 59 ---- .../agent_based/appliance_performance.py} | 44 +-- .../meraki/agent_based/appliance_uplinks.py} | 51 +-- .../meraki/agent_based/appliance_vpns.py} | 31 +- .../meraki/agent_based/cellular_uplinks.py} | 184 +++++----- .../meraki/agent_based/device_uplinks.py} | 23 +- .../meraki/agent_based/networks.py} | 27 +- .../meraki/agent_based/organisations_api.py} | 67 ++-- .../agent_based/switch_ports_statuses.py} | 112 +++--- .../wireless_device_ssid_status.py} | 32 +- .../wireless_ethernet_statuses.py} | 77 ++-- .../meraki/graphing/packages.py | 238 +++++++++++++ .../meraki/lib/agent.py} | 99 ++++-- .../meraki/lib/utils.py} | 56 ++- .../meraki/rulesets/appliance_performance.py | 63 ++++ .../meraki/rulesets/appliance_uplinks.py | 74 ++++ .../meraki/rulesets/appliance_vpns.py | 42 +++ .../meraki/rulesets/licenses_overviewi.py | 100 ++++++ .../meraki/rulesets/organisations.py | 62 ++++ .../meraki/rulesets/organisations_api.py | 62 ++++ .../meraki/rulesets/switch_ports_statuses.py | 158 +++++++++ .../rulesets/wireless_device_ssid_status.py | 43 +++ source/cmk_plugins/cisco/rulesets/meraki.py | 272 ++++++++++++++ .../collection/libexec}/agent_cisco_meraki | 4 +- .../server_side_calls/cisco_meraki.py | 167 +++++++++ source/gui/metrics/cisco_meraki.py | 332 ------------------ .../cisco_meraki_org_appliance_performance.py | 77 ---- .../cisco_meraki_org_appliance_uplinks.py | 82 ----- .../cisco_meraki_org_appliance_vpns.py | 49 --- .../cisco_meraki_org_device_status.py | 35 +- .../cisco_meraki_org_device_status_ps.py | 54 +++ ...cisco_meraki_org_wireless_device_status.py | 47 --- .../cisco_meraki_organisations.py | 56 --- .../cisco_meraki_organisations_api.py | 52 --- .../cisco_meraki_switch_ports_statuses.py | 132 ------- source/packages/cisco_meraki | 67 ++-- source/web/plugins/views/cisco_meraki.py | 9 +- source/web/plugins/wato/agent_cisco_meraki.py | 220 ------------ 44 files changed, 2102 insertions(+), 1564 deletions(-) create mode 100644 mkp/cisco_meraki-1.3.2-20240660.mkp create mode 100644 source/agent_based/cisco_meraki_org_sensor_readings.py_ delete mode 100644 source/checks/agent_cisco_meraki rename source/{agent_based/cisco_meraki_org_appliance_performance.py => cmk_addons_plugins/meraki/agent_based/appliance_performance.py} (65%) rename source/{agent_based/cisco_meraki_org_appliance_uplinks.py => cmk_addons_plugins/meraki/agent_based/appliance_uplinks.py} (86%) rename source/{agent_based/cisco_meraki_org_appliance_vpns.py => cmk_addons_plugins/meraki/agent_based/appliance_vpns.py} (91%) rename source/{agent_based/cisco_meraki_org_cellular_uplinks.py => cmk_addons_plugins/meraki/agent_based/cellular_uplinks.py} (59%) rename source/{agent_based/cisco_meraki_org_device_uplinks.py => cmk_addons_plugins/meraki/agent_based/device_uplinks.py} (77%) rename source/{agent_based/cisco_meraki_org_networks.py => cmk_addons_plugins/meraki/agent_based/networks.py} (87%) rename source/{agent_based/cisco_meraki_organisations_api.py => cmk_addons_plugins/meraki/agent_based/organisations_api.py} (84%) rename source/{agent_based/cisco_meraki_switch_ports_statuses.py => cmk_addons_plugins/meraki/agent_based/switch_ports_statuses.py} (83%) rename source/{agent_based/cisco_meraki_org_wireless_device_status.py => cmk_addons_plugins/meraki/agent_based/wireless_device_ssid_status.py} (88%) rename source/{agent_based/cisco_meraki_org_wireless_ethernet_statuses.py => cmk_addons_plugins/meraki/agent_based/wireless_ethernet_statuses.py} (75%) create mode 100644 source/cmk_addons_plugins/meraki/graphing/packages.py rename source/{lib/python3/cmk/special_agents/agent_cisco_meraki.py => cmk_addons_plugins/meraki/lib/agent.py} (95%) rename source/{agent_based/utils/cisco_meraki.py => cmk_addons_plugins/meraki/lib/utils.py} (65%) create mode 100644 source/cmk_addons_plugins/meraki/rulesets/appliance_performance.py create mode 100644 source/cmk_addons_plugins/meraki/rulesets/appliance_uplinks.py create mode 100644 source/cmk_addons_plugins/meraki/rulesets/appliance_vpns.py create mode 100644 source/cmk_addons_plugins/meraki/rulesets/licenses_overviewi.py create mode 100644 source/cmk_addons_plugins/meraki/rulesets/organisations.py create mode 100644 source/cmk_addons_plugins/meraki/rulesets/organisations_api.py create mode 100644 source/cmk_addons_plugins/meraki/rulesets/switch_ports_statuses.py create mode 100644 source/cmk_addons_plugins/meraki/rulesets/wireless_device_ssid_status.py create mode 100644 source/cmk_plugins/cisco/rulesets/meraki.py rename source/{agents/special => cmk_plugins/collection/libexec}/agent_cisco_meraki (80%) create mode 100644 source/cmk_plugins/collection/server_side_calls/cisco_meraki.py delete mode 100644 source/gui/metrics/cisco_meraki.py delete mode 100644 source/gui/wato/check_parameters/cisco_meraki_org_appliance_performance.py delete mode 100644 source/gui/wato/check_parameters/cisco_meraki_org_appliance_uplinks.py delete mode 100644 source/gui/wato/check_parameters/cisco_meraki_org_appliance_vpns.py create mode 100644 source/gui/wato/check_parameters/cisco_meraki_org_device_status_ps.py delete mode 100644 source/gui/wato/check_parameters/cisco_meraki_org_wireless_device_status.py delete mode 100644 source/gui/wato/check_parameters/cisco_meraki_organisations.py delete mode 100644 source/gui/wato/check_parameters/cisco_meraki_organisations_api.py delete mode 100644 source/gui/wato/check_parameters/cisco_meraki_switch_ports_statuses.py delete mode 100644 source/web/plugins/wato/agent_cisco_meraki.py diff --git a/README.md b/README.md index 50601b0..e5fe350 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[PACKAGE]: ../../raw/master/mkp/cisco_meraki-1.3.2-20240626.mkp "cisco_meraki-1.3.2-20240626.mkp" +[PACKAGE]: ../../raw/master/mkp/cisco_meraki-1.3.2-20240660.mkp "cisco_meraki-1.3.2-20240660.mkp" [SDK]: ../../raw/master/mkp/MerakiSDK-1.46.0-20240516.mkp "MerakiSDK-1.46.0-20240516.mkpp" # Cisco Meraki special agent diff --git a/mkp/cisco_meraki-1.3.2-20240660.mkp b/mkp/cisco_meraki-1.3.2-20240660.mkp new file mode 100644 index 0000000000000000000000000000000000000000..973807245ac69b9b0c0cf90ea75fc8a37f8a4896 GIT binary patch literal 41892 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 G?!N#fkU_iv literal 0 HcmV?d00001 diff --git a/source/agent_based/cisco_meraki_org_device_info.py b/source/agent_based/cisco_meraki_org_device_info.py index d02a911..48aacc9 100644 --- a/source/agent_based/cisco_meraki_org_device_info.py +++ b/source/agent_based/cisco_meraki_org_device_info.py @@ -13,7 +13,7 @@ from dataclasses import dataclass from cmk.base.plugins.agent_based.agent_based_api.v1 import Attributes, register, TableRow, HostLabel from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import InventoryResult, StringTable, HostLabelGenerator -from cmk.base.plugins.agent_based.utils.cisco_meraki import load_json, MerakiAPIData +from cmk_addons.plugins.meraki.lib.utils import load_json, MerakiAPIData @dataclass(frozen=True) @@ -33,7 +33,7 @@ class DeviceInfo: @classmethod def parse(cls, row: MerakiAPIData) -> "DeviceInfo": return cls( - # Some entries may missing in older API versions + # Some entries may be missed in older API versions product=str(row.get("productType", "")), serial=str(row["serial"]), model=str(row["model"]), diff --git a/source/agent_based/cisco_meraki_org_device_status.py b/source/agent_based/cisco_meraki_org_device_status.py index 48ebb61..56b1c6f 100644 --- a/source/agent_based/cisco_meraki_org_device_status.py +++ b/source/agent_based/cisco_meraki_org_device_status.py @@ -36,11 +36,7 @@ from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import ( StringTable, ) -from cmk.base.plugins.agent_based.utils.cisco_meraki import ( - check_last_reported_ts, - load_json, - MerakiAPIData, -) +from cmk_addons.plugins.meraki.lib.utils import MerakiAPIData, check_last_reported_ts, load_json @dataclass(frozen=True) diff --git a/source/agent_based/cisco_meraki_org_licenses_overview.py b/source/agent_based/cisco_meraki_org_licenses_overview.py index 108c7ca..095d28e 100644 --- a/source/agent_based/cisco_meraki_org_licenses_overview.py +++ b/source/agent_based/cisco_meraki_org_licenses_overview.py @@ -37,10 +37,7 @@ from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import ( StringTable, InventoryResult, ) -from cmk.base.plugins.agent_based.utils.cisco_meraki import ( - load_json, - MerakiAPIData, -) +from cmk_addons.plugins.meraki.lib.utils import MerakiAPIData, add_org_id_name_to_output, load_json @dataclass(frozen=True) @@ -120,6 +117,13 @@ def check_licenses_overview( if (item_data := section.get(params.get('internal_item_name', item))) is None: return + yield from add_org_id_name_to_output( + item_data.organisation_id, + item_data.organisation_name, + params['item_variant'], + params.get('dont_show_alias_on_info'), + ) + yield Result( state=State.OK if item_data.status == "OK" else State(params['state_license_not_ok']), summary=f"Status: {item_data.status}", @@ -132,33 +136,10 @@ def check_licenses_overview( licensed_devices = sum(item_data.licensed_device_counts.values()) yield Result( state=State.OK, - summary=f'Number of licensed devices: {licensed_devices}' + summary=f'Licensed devices: {licensed_devices}' ) yield Metric(value=licensed_devices, name='sum_licensed_devices') - org_id = f'ID: {item_data.organisation_id}' - org_name = f'Name: {item_data.organisation_name}' - org_id_notice = f'Organisation ID: {item_data.organisation_id}' - org_name_notice = f'Organisation name: {item_data.organisation_name}' - - match params['item_variant']: - case 'org_id': - yield Result(state=State.OK, notice=org_id_notice) - if params.get('dont_show_alias_on_info'): - yield Result(state=State.OK, notice=org_name_notice) - else: - yield Result(state=State.OK, summary=org_name) - case 'org_name': - if params.get('dont_show_alias_on_info'): - yield Result(state=State.OK, notice=org_id_notice) - else: - yield Result(state=State.OK, summary=org_id) - yield Result(state=State.OK, notice=org_name_notice) - - case _: - yield Result(state=State.OK, notice=org_id_notice) - yield Result(state=State.OK, notice=org_name_notice) - for device_type, device_count in sorted(item_data.licensed_device_counts.items(), key=lambda t: t[0], ): yield Result(state=State.OK, notice=f"{device_type}: {device_count} licensed devices") @@ -192,11 +173,17 @@ def _check_expiration_date( else: yield from check_levels( - age, + value=age, levels_lower=levels_lower, label="Remaining time", render_func=render.timespan, - metric_name="remaining_time" + # metric_name="remaining_time" + ) + # needed as levels don't go the graphing system + yield Metric( + value=age, + name='remaining_time', + levels=levels_lower ) @@ -219,7 +206,7 @@ register.check_plugin( # # inventory license overview # -# ToDo: add senors (MS) +# ToDo: add senors (MT) -> do the need a license? -> done def inventory_licenses_overview(section: Section | None) -> InventoryResult: path = ['software', 'applications', 'cisco_meraki', 'licenses'] for org_id, org_data in section.items(): @@ -233,12 +220,16 @@ def inventory_licenses_overview(section: Section | None) -> InventoryResult: licenses.update({'ms': licenses.get('ms', 0) + device_count}) elif device_type.lower().startswith('mv'): # video / camera licenses.update({'mv': licenses.get('mv', 0) + device_count}) + elif device_type.lower().startswith('mt'): # sensors + licenses.update({'mt': licenses.get('mt', 0) + device_count}) elif device_type.lower().startswith('mr'): # access points licenses.update({'mr': licenses.get('mr', 0) + device_count}) elif device_type.lower().startswith('wireless'): # merge with access points licenses.update({'mr': licenses.get('mr', 0) + device_count}) + elif device_type.lower().startswith('sm'): # systems manager + licenses.update({'sm': licenses.get('sm', 0) + device_count}) else: # fallback for unknown device type - licenses.update({device_type.lower(): device_count}) + licenses.update({device_type.lower(): licenses.get(device_type.lower(), 0) + device_count}) licenses.update({'summary': sum(org_data.licensed_device_counts.values())}) yield TableRow( diff --git a/source/agent_based/cisco_meraki_org_sensor_readings.py_ b/source/agent_based/cisco_meraki_org_sensor_readings.py_ new file mode 100644 index 0000000..10e9d86 --- /dev/null +++ b/source/agent_based/cisco_meraki_org_sensor_readings.py_ @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright (C) 2022 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. + +# enhancements by thl-cmk[at]outlook[dot]com, https://thl-cmk.hopto.org +# 2023-11-10: removed ts check/sort, we get always only the last reading, so need to sort by last reported +# added ability to handle temperature, humidity and battery at the same time, not sure if there +# is a need to add some index (multiple temperature reading for example) +# added battery/humidity check +# changed section from Sequence to Mapping by metric + + +from dataclasses import dataclass +from datetime import datetime +from collections.abc import Sequence, Mapping + +from cmk.base.plugins.agent_based.agent_based_api.v1 import get_value_store, register, Service, render +from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import CheckResult, DiscoveryResult, StringTable +from cmk.base.plugins.agent_based.utils.temperature import check_temperature, TempParamType, check_levels +from cmk_addons.plugins.meraki.lib.utils import check_last_reported_ts, load_json, MerakiAPIData + + +@dataclass(frozen=True) +class SensorReadings: + metric: str + unit: str + last_reported: datetime | None = None + reading: float | None = None + + @classmethod + def parse(cls, row: MerakiAPIData) -> Sequence["SensorReadings"] | None: + if not isinstance(raw_readings := row.get("readings"), list): + return None + + # not needed, we have only the last reading. + # if not ( + # readings_by_datetime := { + # reading_datetime: raw_reading + # for raw_reading in raw_readings + # if (reading_datetime := cls._parse_ts(raw_reading["ts"])) is not None + # } + # ): + # return None + # last_reported, readings = sorted(readings_by_datetime.items(), key=lambda t: t[0], reverse=True)[0] + + parsed_readings = {} + for raw_reading in raw_readings: + sensor_type = raw_reading['metric'] + match sensor_type: + case 'battery': + sensor_unit = 'percentage' + case 'humidity': + sensor_unit = 'relativePercentage' + case 'temperature': + sensor_unit = 'celsius' + case _: + return None + + parsed_readings.update({sensor_type: cls( + last_reported=cls._parse_ts(raw_reading["ts"]), + metric=sensor_type, + unit=sensor_unit, + reading=cls._parse_reading( + reading=raw_reading, + sensor_type=sensor_type, + sensor_unit=sensor_unit, + ), + )}) + + return parsed_readings + + @staticmethod + def _parse_ts(raw_ts: str) -> datetime | None: + try: + return datetime.strptime(raw_ts, "%Y-%m-%dT%H:%M:%SZ") + except ValueError: + return None + + @staticmethod + def _parse_reading(reading: MerakiAPIData, sensor_type: str, sensor_unit: str) -> float | None: + try: + sensor_data = reading[sensor_type] + except KeyError: + return None + + if not isinstance(sensor_data, dict): + return None + + try: + return float(sensor_data[sensor_unit]) + except (KeyError, ValueError): + return None + + +def parse_sensor_readings(string_table: StringTable) -> SensorReadings | None: + return ( + SensorReadings.parse(loaded_json[0]) if (loaded_json := load_json(string_table)) else None + ) + + +register.agent_section( + name="cisco_meraki_org_sensor_readings", + parse_function=parse_sensor_readings, +) + + +def discover_sensor_temperature( + section: Mapping[SensorReadings] | None, +) -> DiscoveryResult: + if 'temperature' in section.keys(): + yield Service(item='Temperature') + + +def check_sensor_temperature( + item: str, + params: TempParamType, + section: Mapping[SensorReadings] | None, +) -> CheckResult: + try: + reading = section[item.lower()] + except KeyError: + return None + + yield from check_temperature( + reading=reading.reading, + params=params, + unique_name=item, + value_store=get_value_store(), + ) + + if reading.last_reported is not None: + yield from check_last_reported_ts( + last_reported_ts=reading.last_reported.timestamp(), + as_metric=False, + ) + + +register.check_plugin( + name="cisco_meraki_org_sensor_temperature", + sections=["cisco_meraki_org_sensor_readings"], + service_name="Sensor %s", + discovery_function=discover_sensor_temperature, + check_function=check_sensor_temperature, + check_ruleset_name="temperature", + check_default_parameters={ + # "levels": (50.0, 60.0), + }, +) + + +def discover_sensor_humidity( + section: Mapping[SensorReadings] | None, +) -> DiscoveryResult: + if 'humidity' in section.keys(): + yield Service(item='Humidity') + + +def check_sensor_humidity( + item: str, + params: TempParamType, + section: Mapping[SensorReadings] | None, +) -> CheckResult: + try: + reading = section[item.lower()] + except KeyError: + return None + + yield from check_levels( + value=reading.reading, + label="Relative Humidity", + levels_upper=params.get('levels_upper', None), + metric_name='humidity', + render_func=render.percent + ) + + if reading.last_reported is not None: + yield from check_last_reported_ts( + last_reported_ts=reading.last_reported.timestamp(), + as_metric=False, + ) + + +register.check_plugin( + name="cisco_meraki_org_sensor_humidity", + sections=["cisco_meraki_org_sensor_readings"], + service_name="Sensor %s", + discovery_function=discover_sensor_humidity, + check_function=check_sensor_humidity, + check_ruleset_name="humidity", + check_default_parameters={ + # "levels": (50.0, 60.0), + }, +) + + +def discover_sensor_battery( + section: Mapping[SensorReadings] | None, +) -> DiscoveryResult: + if 'battery' in section.keys(): + yield Service(item='Battery') + + +def check_sensor_battery( + item: str, + params: TempParamType, + section: Mapping[SensorReadings] | None, +) -> CheckResult: + try: + reading = section[item.lower()] + except KeyError: + return None + + yield from check_levels( + value=reading.reading, + label='Battery level', + levels_upper=params.get('levels_upper', None), + metric_name='battery', + render_func=render.percent + ) + + if reading.last_reported is not None: + yield from check_last_reported_ts( + last_reported_ts=reading.last_reported.timestamp(), + as_metric=False, + ) + + +register.check_plugin( + name="cisco_meraki_org_sensor_battery", + sections=["cisco_meraki_org_sensor_readings"], + service_name="Sensor %s", + discovery_function=discover_sensor_battery, + check_function=check_sensor_battery, + check_ruleset_name="battery", + check_default_parameters={ + # "levels": (50.0, 60.0), + }, +) diff --git a/source/checks/agent_cisco_meraki b/source/checks/agent_cisco_meraki deleted file mode 100644 index 74230b8..0000000 --- a/source/checks/agent_cisco_meraki +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (C) 2022 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. - -# enhancements by thl-cmk[at]outlook[dot]com, https://thl-cmk.hopto.org -# - added host_suffix_prefix option -# - added no-cache option -# 2023-11-18: changed from section to excluded_sections -# 2023-11-22: replaced host_suffix_prefix option by org_id_as_prefix -# 2024-06-23: added cache time per section -> not nice but should work. - -# -# needs to be re implemented for CMK 2.3.X -# https://github.com/Checkmk/checkmk/commit/c12cca9fe631d935ed5f239c23288ea856869e6e -# - -from collections.abc import Mapping, Sequence -from typing import Any - - -def agent_cisco_meraki_arguments( - params: Mapping[str, Any], - hostname: str, - ipaddress: str | None, -) -> Sequence[object]: - args = [ - hostname, - passwordstore_get_cmdline("%s", params["api_key"]), - ] - - if (proxy := params.get("proxy")) is not None: - args.extend( - [ - "--proxy", - get_http_proxy(proxy).serialize(), - ] - ) - - if orgs := params.get("orgs"): - args.extend(["--orgs"] + orgs) - - if params.get("no_cache"): - args.append('--no-cache') - - if params.get("org_id_as_prefix"): - args.append('--org-id-as-prefix') - - if excluded_sections := params.get("excluded_sections"): - args.extend(["--excluded-sections"] + excluded_sections) - - if cache_per_section := params.get("cache_per_section"): - args.extend(["--cache-per-section"] + list(cache_per_section)) - - return args - - -special_agent_info["cisco_meraki"] = agent_cisco_meraki_arguments diff --git a/source/agent_based/cisco_meraki_org_appliance_performance.py b/source/cmk_addons_plugins/meraki/agent_based/appliance_performance.py similarity index 65% rename from source/agent_based/cisco_meraki_org_appliance_performance.py rename to source/cmk_addons_plugins/meraki/agent_based/appliance_performance.py index 1ce4e1d..fce7cf5 100644 --- a/source/agent_based/cisco_meraki_org_appliance_performance.py +++ b/source/cmk_addons_plugins/meraki/agent_based/appliance_performance.py @@ -5,33 +5,35 @@ # # Author: thl-cmk[at]outlook[dot]com # URL : https://thl-cmk.hopto.org -# Date : 2023-11-04 +# Date : 2024-06-20 # File : cisco_meraki_appliance_performance.py (check plugin) +# 2024-06-29: refactored for CMK 2.3 +# 2024-06-30: renamed from cisco_meraki_org_appliance_performance.py in to appliance_performance.py -from _collections_abc import Mapping +from collections.abc import Mapping -from cmk.base.plugins.agent_based.agent_based_api.v1 import ( - register, - check_levels, - Service, - render, -) -from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import ( +from cmk.agent_based.v2 import ( + AgentSection, + CheckPlugin, CheckResult, DiscoveryResult, + Service, StringTable, + check_levels, + render, ) -from cmk.base.plugins.agent_based.utils.cisco_meraki import ( - load_json, -) + +from cmk_addons.plugins.meraki.lib.utils import load_json + # sample agent output # {"perfScore": 1} # sample string_table # [['[{"perfScore": 0}]']] -def parse_appliance_performance(string_table: StringTable) -> int: + +def parse_appliance_performance(string_table: StringTable) -> int | None: json_data = load_json(string_table) if (json_data := json_data[0]) is None: return @@ -41,7 +43,7 @@ def parse_appliance_performance(string_table: StringTable) -> int: return int(perfscore) -register.agent_section( +agent_section_meraki_org_appliance_performance = AgentSection( name="cisco_meraki_org_appliance_performance", parse_function=parse_appliance_performance, ) @@ -55,20 +57,20 @@ def check_appliance_performance(params: Mapping[str, any], section: int) -> Chec yield from check_levels( value=section, label='Utilization', - levels_upper=params.get('levels_upper'), + levels_upper=params['levels_upper'], render_func=render.percent, metric_name='utilization', boundaries=(0, 100), ) -register.check_plugin( - name='cisco_meraki_org_appliance_performance', - service_name='Utilization', - discovery_function=discover_appliance_performance, +check_plugin_meraki_org_appliance_performance = CheckPlugin( + name="cisco_meraki_org_appliance_performance", + service_name="Utilization", check_function=check_appliance_performance, + discovery_function=discover_appliance_performance, + check_ruleset_name="cisco_meraki_org_appliance_performance", check_default_parameters={ - 'levels_upper': (60, 80), + 'levels_upper': ('fixed', (60, 80)), }, - check_ruleset_name='cisco_meraki_org_appliance_performance', ) diff --git a/source/agent_based/cisco_meraki_org_appliance_uplinks.py b/source/cmk_addons_plugins/meraki/agent_based/appliance_uplinks.py similarity index 86% rename from source/agent_based/cisco_meraki_org_appliance_uplinks.py rename to source/cmk_addons_plugins/meraki/agent_based/appliance_uplinks.py index 5bfe043..6879d6c 100644 --- a/source/agent_based/cisco_meraki_org_appliance_uplinks.py +++ b/source/cmk_addons_plugins/meraki/agent_based/appliance_uplinks.py @@ -15,28 +15,29 @@ # moved parse function to the dataclasses # 2024-05-19: reworked appliance uplinks usage # 2024-04-24: fixed, we can have no traffic if uplinc is not connected +# 2024-06-29: refactored for CMK 2.3 +# changed service name from "Appliance Uplink" to "Uplink" +# fixed render function for bandwidth -> uses now render.networkbandwidth +# 2024-06-30: renamed from cisco_meraki_org_appliance_uplinks.py in to appliance_uplinks.py +from collections.abc import Mapping from dataclasses import dataclass from datetime import datetime -from _collections_abc import Mapping -from cmk.base.plugins.agent_based.agent_based_api.v1 import ( +from cmk.agent_based.v2 import ( + AgentSection, + CheckPlugin, + CheckResult, + DiscoveryResult, Result, Service, State, + StringTable, check_levels, - register, render, ) -from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import ( - CheckResult, - DiscoveryResult, - StringTable, -) -from cmk.base.plugins.agent_based.utils.cisco_meraki import ( - get_int, # type: ignore[import] - load_json, -) + +from cmk_addons.plugins.meraki.lib.utils import get_int, load_json # sample string_table __appliance_uplinks = [ @@ -150,7 +151,7 @@ def parse_appliance_uplinks(string_table: StringTable) -> Appliance | None: return Appliance.parse(json_data[0]) -register.agent_section( +agent_section_cisco_meraki_org_appliance_uplinks = AgentSection( name="cisco_meraki_org_appliance_uplinks", parse_function=parse_appliance_uplinks, ) @@ -164,21 +165,23 @@ def discover_appliance_uplinks(section: Appliance) -> DiscoveryResult: _STATUS_MAP = { "active": 0, "failed": 2, - "not connected": 1, + "not_connected": 1, "ready": 0, } - _TIMESPAN = 60 +def render_network_bandwidth_bits(value: int) -> str: + return render.networkbandwidth(value/8) + + def check_appliance_uplinks(item: str, params: Mapping[str, any], section: Appliance) -> CheckResult: - try: - uplink: ApplianceUplink = section.uplinks[item] - except KeyError: + if (uplink := section.uplinks.get(item)) is None: return None if params.get('status_map'): _STATUS_MAP.update(params['status_map']) + _STATUS_MAP['not connected'] = _STATUS_MAP['not_connected'] # can not use 'nor connected' in params anymore :-( yield Result(state=State(_STATUS_MAP.get(uplink.status, 3)), summary=f'Status: {uplink.status}') if uplink.ip: @@ -187,15 +190,14 @@ def check_appliance_uplinks(item: str, params: Mapping[str, any], section: Appli yield Result(state=State.OK, summary=f'Public IP: {uplink.public_ip}') yield Result(state=State.OK, notice=f'Network: {section.network_name}') - if uplink.status in ['active']: # we can only have traffic, if uplinc is connected + if params.get('show_traffic') and uplink.status in ['active']: # we can only have traffic, if uplink is connected if uplink.received: # and params.get('show_traffic'): value = uplink.received * 8 / _TIMESPAN # Bits / Timespan yield from check_levels( value=value, # Bits label='In', metric_name='if_in_bps', - render_func=lambda v: render.networkbandwidth(v/8), # Bytes - # notice_only=True, + render_func=render_network_bandwidth_bits, # Bytes ) if uplink.sent: # and params.get('show_traffic'): @@ -204,8 +206,7 @@ def check_appliance_uplinks(item: str, params: Mapping[str, any], section: Appli value=value, # Bits label='Out', metric_name='if_out_bps', - render_func=lambda v: render.networkbandwidth(v/8), # Bytes - # notice_only=True, + render_func=render_network_bandwidth_bits, # Bytes ) # not needed, will show in device status (?) @@ -225,9 +226,9 @@ def check_appliance_uplinks(item: str, params: Mapping[str, any], section: Appli yield Result(state=State.OK, notice=f'Secondary DNS: {uplink.secondary_dns}') -register.check_plugin( +check_plugin_cisco_meraki_org_appliance_uplinks = CheckPlugin( name='cisco_meraki_org_appliance_uplinks', - service_name='Appliance Uplink %s', + service_name='Uplink %s', discovery_function=discover_appliance_uplinks, check_function=check_appliance_uplinks, check_default_parameters={}, diff --git a/source/agent_based/cisco_meraki_org_appliance_vpns.py b/source/cmk_addons_plugins/meraki/agent_based/appliance_vpns.py similarity index 91% rename from source/agent_based/cisco_meraki_org_appliance_vpns.py rename to source/cmk_addons_plugins/meraki/agent_based/appliance_vpns.py index 25b76d1..e0856fe 100644 --- a/source/agent_based/cisco_meraki_org_appliance_vpns.py +++ b/source/cmk_addons_plugins/meraki/agent_based/appliance_vpns.py @@ -10,25 +10,26 @@ # 2024-04-27: made data parsing more robust # 2024-05-15: moved parse function to data classes +# 2024-06-29: refactored for CMK 2.3 +# changed service name from "Appliance VPN" to "VPN peer" +# 2024-06-30: renamed from cisco_meraki_org_appliance_vpns.py in to appliance_vpns.py from abc import abstractmethod +from collections.abc import Mapping, Sequence from dataclasses import dataclass -from _collections_abc import Mapping, Sequence -from cmk.base.plugins.agent_based.agent_based_api.v1 import ( - register, +from cmk.agent_based.v2 import ( + AgentSection, + CheckPlugin, + CheckResult, + DiscoveryResult, Result, Service, State, -) -from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import ( - CheckResult, - DiscoveryResult, StringTable, ) -from cmk.base.plugins.agent_based.utils.cisco_meraki import ( - load_json, -) + +from cmk_addons.plugins.meraki.lib.utils import load_json # sample string_table __appliance_vpn_statuses = [ @@ -163,7 +164,7 @@ def parse_appliance_vpns(string_table: StringTable) -> Mapping[str, ApplianceVPN return meraki_peers -register.agent_section( +agent_section_cisco_meraki_org_appliance_vpns = AgentSection( name="cisco_meraki_org_appliance_vpns", parse_function=parse_appliance_vpns, ) @@ -175,9 +176,7 @@ def discover_appliance_vpns(section: Mapping[str, ApplianceVPNPeer]) -> Discover def check_appliance_vpns(item: str, params: Mapping[str, any], section: Mapping[str, ApplianceVPNPeer]) -> CheckResult: - try: - peer: ApplianceVPNPeer = section[item] - except KeyError: + if (peer := section.get(item)) is None: return None if peer.reachability is not None and peer.reachability.lower() in ['reachable']: @@ -198,9 +197,9 @@ def check_appliance_vpns(item: str, params: Mapping[str, any], section: Mapping[ yield Result(state=State.OK, notice=f'name: {uplink.interface}, public IP: {uplink.public_ip}') -register.check_plugin( +check_plugin_cisco_meraki_org_appliance_vpns = CheckPlugin( name='cisco_meraki_org_appliance_vpns', - service_name='Appliance VPN %s', + service_name='VPN peer %s', discovery_function=discover_appliance_vpns, check_function=check_appliance_vpns, check_default_parameters={}, diff --git a/source/agent_based/cisco_meraki_org_cellular_uplinks.py b/source/cmk_addons_plugins/meraki/agent_based/cellular_uplinks.py similarity index 59% rename from source/agent_based/cisco_meraki_org_cellular_uplinks.py rename to source/cmk_addons_plugins/meraki/agent_based/cellular_uplinks.py index 0261165..06c149e 100644 --- a/source/agent_based/cisco_meraki_org_cellular_uplinks.py +++ b/source/cmk_addons_plugins/meraki/agent_based/cellular_uplinks.py @@ -9,30 +9,63 @@ # File : cisco_meraki_org_cellular_uplinks.py (check plugin) # 2024-04-27: made data parsing more robust +# 2024-06-29: refactored for CMK 2.3 +# moved parse functions to class methods +# changed service name from "Cellular uplink" to "Uplink" +# 2024-06-30: renamed from cisco_meraki_org_cellular_uplinks.py in to cellular_uplinks.py - -from _collections_abc import Mapping +from collections.abc import Mapping from dataclasses import dataclass from datetime import datetime -from cmk.base.plugins.agent_based.agent_based_api.v1 import ( +from cmk.agent_based.v2 import ( + AgentSection, + CheckPlugin, + CheckResult, + DiscoveryResult, Metric, Result, Service, State, - register, - render, -) -from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import ( - CheckResult, - DiscoveryResult, StringTable, + render, ) -from cmk.base.plugins.agent_based.utils.cisco_meraki import ( - get_int, # type: ignore[import] - load_json, -) +from cmk_addons.plugins.meraki.lib.utils import get_int, load_json + +__cellular_uplinks = [ + { + "highAvailability": { + "enabled": False, + "role": "primary" + }, + "lastReportedAt": "2023-11-13T19:52:06Z", + "model": "MG41", + "networkId": "L_575897802350012343", + "serial": "QQQQ-XXXX-ZZZZ", + "uplinks": [ + { + "apn": "apn.name", + "connectionType": "lte", + "dns1": None, + "dns2": None, + "gateway": None, + "iccid": "89492027206012345518", + "interface": "cellular", + "ip": None, + "model": "integrated", + "provider": "provider.name", + "publicIp": "2.3.4.5", + "signalStat": { + "rsrp": "-111", + "rsrq": "-8" + }, + "signalType": None, + "status": "active" + } + ] + } +] _LAST_REPORTED_AT = "%Y-%m-%dT%H:%M:%SZ" @@ -57,12 +90,41 @@ class CellularUplink: signal_type: str | None status: str | None + @classmethod + def parse(cls, uplink: Mapping[str, object]): + return cls( + apn=str(uplink['apn']) if uplink.get('apn') is not None else None, + connection_type=str(uplink['connectionType']) if uplink.get('connectionType') is not None else None, + dns1=str(uplink['dns1']) if uplink.get('dns1') is not None else None, + dns2=str(uplink['dns2']) if uplink.get('dns2') is not None else None, + gateway=str(uplink['gateway']) if uplink.get('gateway') is not None else None, + iccid=str(uplink['iccid']) if uplink.get('iccid') is not None else None, + interface=str(uplink['interface']) if uplink.get('interface') is not None else None, + ip=str(uplink['ip']) if uplink.get('ip') is not None else None, + model=str(uplink['model']) if uplink.get('model') is not None else None, + provider=str(uplink['provider']) if uplink.get('provider') is not None else None, + public_ip=str(uplink['publicIp']) if uplink.get('publicIp') is not None else None, + signal_type=str(uplink['signalType']) if uplink.get('signalType') is not None else None, + status=str(uplink['status']) if uplink.get('status') is not None else None, + rsrp=get_int(uplink.get('signalStat', {}).get('rsrp')), + rsrq=get_int(uplink.get('signalStat', {}).get('rsrq')), + received=get_int(uplink.get('received')), + sent=get_int(uplink.get('sent')), + ) + @dataclass(frozen=True) class CellularUplinkHA: enabled: bool | None role: str | None + @classmethod + def parse(cls, high_availability: Mapping[str, object]): + return cls( + enabled=bool(high_availability['enabled']) if high_availability.get('enabled') is not None else None, + role=str(high_availability['role']) if high_availability.get('enabled') is not None else None, + ) + @dataclass(frozen=True) class CellularGateway: @@ -73,82 +135,30 @@ class CellularGateway: serial: str | None uplinks: Mapping[str, CellularUplink] | None - -__cellular_uplinks = [ - { - "highAvailability": { - "enabled": False, - "role": "primary" - }, - "lastReportedAt": "2023-11-13T19:52:06Z", - "model": "MG41", - "networkId": "L_575897802350012343", - "serial": "QQQQ-XXXX-ZZZZ", - "uplinks": [ - { - "apn": "apn.name", - "connectionType": "lte", - "dns1": None, - "dns2": None, - "gateway": None, - "iccid": "89492027206012345518", - "interface": "cellular", - "ip": None, - "model": "integrated", - "provider": "provider.name", - "publicIp": "2.3.4.5", - "signalStat": { - "rsrp": "-111", - "rsrq": "-8" - }, - "signalType": None, - "status": "active" - } - ] - } -] + @classmethod + def parse(cls, cellular_gateway): + return cls( + serial=str(cellular_gateway['serial']) if cellular_gateway.get('serial') is not None else None, + model=str(cellular_gateway['model']) if cellular_gateway.get('model') is not None else None, + last_reported_at=datetime.strptime( + cellular_gateway['lastReportedAt'], _LAST_REPORTED_AT) if cellular_gateway.get( + 'lastReportedAt' + ) is not None else None, + # network_name=str(json_data['networkName']) if json_data.get('networkName') is not None else None, + high_availability=CellularUplinkHA.parse(cellular_gateway.get('highAvailability', {})), + uplinks={ + uplink['interface']: CellularUplink.parse(uplink) for uplink in cellular_gateway.get('uplinks', []) + }, + ) def parse_cellular_uplinks(string_table: StringTable) -> CellularGateway | None: json_data = load_json(string_table) json_data = json_data[0] - return CellularGateway( - serial=str(json_data['serial']) if json_data.get('serial') is not None else None, - model=str(json_data['model']) if json_data.get('model') is not None else None, - last_reported_at=datetime.strptime(json_data['lastReportedAt'], _LAST_REPORTED_AT) if json_data.get( - 'lastReportedAt') is not None else None, - # network_name=str(json_data['networkName']) if json_data.get('networkName') is not None else None, - high_availability=CellularUplinkHA( - enabled=bool(json_data['highAvailability']['enabled']) if json_data.get('highAvailability', {}).get( - 'enabled') is not None else None, - role=str(json_data['highAvailability']['role']) if json_data.get('highAvailability', {}).get( - 'enabled') is not None else None, - ), - uplinks={ - uplink['interface']: CellularUplink( - apn=str(uplink['apn']) if uplink.get('apn') is not None else None, - connection_type=str(uplink['connectionType']) if uplink.get('connectionType') is not None else None, - dns1=str(uplink['dns1']) if uplink.get('dns1') is not None else None, - dns2=str(uplink['dns2']) if uplink.get('dns2') is not None else None, - gateway=str(uplink['gateway']) if uplink.get('gateway') is not None else None, - iccid=str(uplink['iccid']) if uplink.get('iccid') is not None else None, - interface=str(uplink['interface']) if uplink.get('interface') is not None else None, - ip=str(uplink['ip']) if uplink.get('ip') is not None else None, - model=str(uplink['model']) if uplink.get('model') is not None else None, - provider=str(uplink['provider']) if uplink.get('provider') is not None else None, - public_ip=str(uplink['publicIp']) if uplink.get('publicIp') is not None else None, - signal_type=str(uplink['signalType']) if uplink.get('signalType') is not None else None, - status=str(uplink['status']) if uplink.get('status') is not None else None, - rsrp=get_int(uplink.get('signalStat', {}).get('rsrp')), - rsrq=get_int(uplink.get('signalStat', {}).get('rsrq')), - received=get_int(uplink.get('received')), - sent=get_int(uplink.get('sent')), - ) for uplink in json_data.get('uplinks', []) - }, - ) + return CellularGateway.parse(json_data) -register.agent_section( +agent_section_cisco_meraki_org_cellular_uplinks = AgentSection( name="cisco_meraki_org_cellular_uplinks", parse_function=parse_cellular_uplinks, ) @@ -160,10 +170,8 @@ def discover_cellular_uplinks(section: CellularGateway) -> DiscoveryResult: def check_cellular_uplinks(item: str, params: Mapping[str, any], section: CellularGateway) -> CheckResult: - try: - uplink: CellularUplink = section.uplinks[item] - except KeyError: - return None + if (uplink := section.uplinks.get(item)) is None: + return if uplink.status not in ['active']: yield Result(state=State(params.get('status_not_active', 1)), summary=f'Status: {uplink.status}') @@ -206,9 +214,9 @@ def check_cellular_uplinks(item: str, params: Mapping[str, any], section: Cellul yield Result(state=State.OK, notice=f'DNS 2: {uplink.dns2}') -register.check_plugin( +check_plugin_cisco_meraki_org_cellular_uplinks = CheckPlugin( name='cisco_meraki_org_cellular_uplinks', - service_name='Cellular Uplink %s', + service_name='Uplink %s', discovery_function=discover_cellular_uplinks, check_function=check_cellular_uplinks, check_default_parameters={}, diff --git a/source/agent_based/cisco_meraki_org_device_uplinks.py b/source/cmk_addons_plugins/meraki/agent_based/device_uplinks.py similarity index 77% rename from source/agent_based/cisco_meraki_org_device_uplinks.py rename to source/cmk_addons_plugins/meraki/agent_based/device_uplinks.py index 47b181c..26ccfc8 100644 --- a/source/agent_based/cisco_meraki_org_device_uplinks.py +++ b/source/cmk_addons_plugins/meraki/agent_based/device_uplinks.py @@ -11,12 +11,21 @@ # inventory of cisco Meraki uplinks # 2024-04-27: made data parsing more robust +# 2024-06-29: refactored for CMK 2.3 +# 2024-06-30: renamed from cisco_meraki_org_device_uplinks.py in to device_uplinks.py + from collections.abc import Sequence -from cmk.base.plugins.agent_based.agent_based_api.v1 import register, TableRow -from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import InventoryResult, StringTable -from cmk.base.plugins.agent_based.utils.cisco_meraki import load_json +from cmk.agent_based.v2 import ( + AgentSection, + InventoryPlugin, + InventoryResult, + StringTable, + TableRow, +) + +from cmk_addons.plugins.meraki.lib.utils import load_json __uplinks = [ { @@ -40,7 +49,7 @@ def parse_device_uplinks(string_table: StringTable) -> Sequence | None: return loaded_json[0]['uplinks'] if (loaded_json := load_json(string_table)) else None -register.agent_section( +agent_section_cisco_meraki_org_device_uplinks_info = AgentSection( name="cisco_meraki_org_device_uplinks_info", parse_function=parse_device_uplinks, ) @@ -58,7 +67,9 @@ def inventory_device_uplinks(section: Sequence | None) -> InventoryResult: **({"address": str(address['address'])} if address.get('address') is not None else {}), } inventory_columns = { - **({"assignment_mode": str(address['assignmentMode'])} if address.get('assignmentMode') is not None else {}), + **({"assignment_mode": str( + address['assignmentMode'] + )} if address.get('assignmentMode') is not None else {}), **({"gateway": str(address['gateway'])} if address.get('gateway') is not None else {}), **({"public_address": str(address['public']['address'])} if address.get('public', {}).get( 'address') is not None else {}), @@ -70,7 +81,7 @@ def inventory_device_uplinks(section: Sequence | None) -> InventoryResult: ) -register.inventory_plugin( +inventory_plugin_cisco_meraki_org_device_uplinks_info = InventoryPlugin( name="cisco_meraki_org_device_uplinks_info", inventory_function=inventory_device_uplinks, ) diff --git a/source/agent_based/cisco_meraki_org_networks.py b/source/cmk_addons_plugins/meraki/agent_based/networks.py similarity index 87% rename from source/agent_based/cisco_meraki_org_networks.py rename to source/cmk_addons_plugins/meraki/agent_based/networks.py index 945396d..cd98c3c 100644 --- a/source/agent_based/cisco_meraki_org_networks.py +++ b/source/cmk_addons_plugins/meraki/agent_based/networks.py @@ -8,23 +8,22 @@ # Date : 2023-11-04 # File : cisco_meraki_org_networks.py (check plugin) -from dataclasses import dataclass +# 2024-06-29: refactored for CMK 2.3 +# 2024-06-30: renamed from cisco_meraki_org_networks.py in to networks.py + from collections.abc import Sequence +from dataclasses import dataclass from typing import Final -from cmk.base.plugins.agent_based.agent_based_api.v1 import ( - TableRow, - register, -) -from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import ( +from cmk.agent_based.v2 import ( + AgentSection, + InventoryPlugin, InventoryResult, StringTable, + TableRow, ) -from cmk.base.plugins.agent_based.utils.cisco_meraki import ( - MerakiAPIData, - MerakiNetwork, # type: ignore[import] - load_json, -) +from cmk_addons.plugins.meraki.lib.utils import MerakiAPIData, MerakiNetwork, load_json + _API_NAME_ORGANISATION_NAME: Final = "name" @@ -50,7 +49,7 @@ class NetworkInfo(MerakiNetwork): url: str @classmethod - def parse(cls, organisations: MerakiAPIData) -> "NetworkInfo": + def parse(cls, organisations: MerakiAPIData): networks = [] for organisation in organisations: for network in organisation: @@ -74,7 +73,7 @@ def parse_meraki_networks(string_table: StringTable) -> Sequence[NetworkInfo] | return NetworkInfo.parse(loaded_json) if (loaded_json := load_json(string_table)) else None -register.agent_section( +agent_section_cisco_meraki_org_networks = AgentSection( name="cisco_meraki_org_networks", parse_function=parse_meraki_networks, ) @@ -105,7 +104,7 @@ def inventory_meraki_networks(section: Sequence[NetworkInfo] | None) -> Inventor ) -register.inventory_plugin( +inventory_plugin_cisco_meraki_org_networks = InventoryPlugin( name="cisco_meraki_org_networks", inventory_function=inventory_meraki_networks, ) diff --git a/source/agent_based/cisco_meraki_organisations_api.py b/source/cmk_addons_plugins/meraki/agent_based/organisations_api.py similarity index 84% rename from source/agent_based/cisco_meraki_organisations_api.py rename to source/cmk_addons_plugins/meraki/agent_based/organisations_api.py index 130754d..dd06601 100644 --- a/source/agent_based/cisco_meraki_organisations_api.py +++ b/source/cmk_addons_plugins/meraki/agent_based/organisations_api.py @@ -11,28 +11,28 @@ # 2024-04-27: made data parsing more robust # 2024-05-12: added api request count # refactoring parse functions as class method +# 2024-06-29: refactoring for CMK 2.3 +# 2024-06-30: renamed from cisco_meraki_organisations_api.py in to organisations_api.py -from _collections_abc import Mapping, Sequence +from collections.abc import Mapping, Sequence from dataclasses import dataclass -from cmk.base.plugins.agent_based.agent_based_api.v1 import ( +from cmk.agent_based.v2 import ( + AgentSection, + CheckPlugin, + CheckResult, + DiscoveryResult, + InventoryPlugin, + InventoryResult, Result, Service, State, + StringTable, TableRow, check_levels, - register, -) -from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import ( - CheckResult, - DiscoveryResult, - InventoryResult, - StringTable, -) -from cmk.base.plugins.agent_based.utils.cisco_meraki import ( - get_int, # type: ignore[import] - load_json, ) +from cmk_addons.plugins.meraki.lib.utils import add_org_id_name_to_output, get_int, load_json + __orgaisations = [ {'id': '610473', @@ -122,7 +122,7 @@ def parse_meraki_organisations(string_table: StringTable) -> Mapping[str, Organi return {org['id']: Organisation.parse(org) for org in json_data} -register.agent_section( +agent_section_cisco_meraki_org_organisations = AgentSection( name='cisco_meraki_org_organisations', parsed_section_name='cisco_meraki_organisations_api', parse_function=parse_meraki_organisations, @@ -140,7 +140,7 @@ def parse_cisco_meraki_org_api_requests_by_organization( } -register.agent_section( +agent_section_cisco_meraki_org_api_requests_by_organization = AgentSection( name='cisco_meraki_org_api_requests_by_organization', parsed_section_name='cisco_meraki_org_api_requests_by_organization', parse_function=parse_cisco_meraki_org_api_requests_by_organization, @@ -164,7 +164,7 @@ def inventory_meraki_organisations(section: Mapping[str, Organisation]) -> Inven ) -register.inventory_plugin( +inventory_plugin_cisco_meraki_organisations_api = InventoryPlugin( name='cisco_meraki_organisations_api', # sections=['cisco_meraki_organisations'], inventory_function=inventory_meraki_organisations, @@ -202,32 +202,19 @@ def check_organisations_api( ) -> CheckResult: if (org := section_cisco_meraki_organisations_api.get(params.get('internal_item_name'))) is None: return + + yield from add_org_id_name_to_output( + org.id, + org.name, + params['item_variant'], + params.get('dont_show_alias_on_info'), + ) + yield Result( state=State.OK if org.api else State(params['state_api_not_enabled']), - summary=f'Status: {_api_status[org.api]}', + summary=f'({_api_status[org.api]})', + details=f'Status: {_api_status[org.api]}', ) - org_id = f'ID: {org.id}' - org_name = f'Name: {org.name}' - org_id_notice = f'Organisation ID: {org.id}' - org_name_notice = f'Organisation name: {org.name}' - - match params['item_variant']: - case 'org_id': - yield Result(state=State.OK, notice=org_id_notice) - if params.get('dont_show_alias_on_info'): - yield Result(state=State.OK, notice=org_name_notice) - else: - yield Result(state=State.OK, summary=org_name) - case 'org_name': - if params.get('dont_show_alias_on_info'): - yield Result(state=State.OK, notice=org_id_notice) - else: - yield Result(state=State.OK, summary=org_id) - yield Result(state=State.OK, notice=org_name_notice) - - case _: - yield Result(state=State.OK, notice=org_id_notice) - yield Result(state=State.OK, notice=org_name_notice) if section_cisco_meraki_org_api_requests_by_organization is None or ( api_requests := section_cisco_meraki_org_api_requests_by_organization.get(params.get( @@ -266,7 +253,7 @@ def check_organisations_api( ) -register.check_plugin( +check_plugin_cisco_meraki_organisations_api = CheckPlugin( name='cisco_meraki_organisations_api', sections=['cisco_meraki_organisations_api', 'cisco_meraki_org_api_requests_by_organization'], service_name='Cisco Meraki API %s', diff --git a/source/agent_based/cisco_meraki_switch_ports_statuses.py b/source/cmk_addons_plugins/meraki/agent_based/switch_ports_statuses.py similarity index 83% rename from source/agent_based/cisco_meraki_switch_ports_statuses.py rename to source/cmk_addons_plugins/meraki/agent_based/switch_ports_statuses.py index b9673ef..8409609 100644 --- a/source/agent_based/cisco_meraki_switch_ports_statuses.py +++ b/source/cmk_addons_plugins/meraki/agent_based/switch_ports_statuses.py @@ -14,32 +14,34 @@ # added support for "realtime" traffic counters # refactoring parse functions as class method # 2024-05-20: added discovery rule for port status +# 2024-06-29: refactored for CMK 2.3 +# fixed render function for bandwidth -> uses now render.networkbandwidth +# try to match the output of a "normal" cmk interface service +# 2024-06-30: renamed from cisco_meraki_switch_ports_statuses.py in to switch_ports_statuses.py +# 2024-06-30: fixed discovery of (admin disabled) ports +# ToDo: create service label cmk/meraki/uplink:yes/no -from _collections_abc import Mapping, Sequence +from collections.abc import Mapping, Sequence from dataclasses import dataclass -from cmk.base.plugins.agent_based.agent_based_api.v1 import ( +from cmk.agent_based.v2 import ( + AgentSection, + CheckPlugin, + CheckResult, + DiscoveryResult, + InventoryPlugin, + InventoryResult, Result, Service, State, + StringTable, TableRow, check_levels, - register, render, ) -from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import ( - CheckResult, - DiscoveryResult, - InventoryResult, - StringTable, -) -from cmk.base.plugins.agent_based.utils.cisco_meraki import ( - get_float, # type: ignore[import] - get_int, # type: ignore[import] - load_json, -) +from cmk_addons.plugins.meraki.lib.utils import get_float, get_int, load_json @dataclass(frozen=True) @@ -236,7 +238,7 @@ def parse_switch_ports_statuses(string_table: StringTable) -> Mapping[str, Switc return {port['portId']: SwitchPort.parse(port) for port in json_data} -register.agent_section( +agent_section_cisco_meraki_org_switch_ports_statuses = AgentSection( name="cisco_meraki_org_switch_ports_statuses", parse_function=parse_switch_ports_statuses, ) @@ -244,18 +246,31 @@ register.agent_section( def discover_switch_ports_statuses(params: Mapping[str, object], section: Mapping[str, SwitchPort]) -> DiscoveryResult: discovered_port_states = params['discovered_port_states'] - for port in section.keys(): - if section[port].enabled in discovered_port_states and section[port].status in discovered_port_states: + # adjust params, as we can not use True/False as keys anymore in rule sets :-( + if 'admin_enabled' in discovered_port_states: + discovered_port_states.append(True) + discovered_port_states.remove('admin_enabled') + if 'admin_disabled' in discovered_port_states: + discovered_port_states.append(False) + discovered_port_states.append('disabled') + discovered_port_states.remove('admin_disabled') + + for port in section.values(): + if port.enabled in discovered_port_states and port.status.lower() in discovered_port_states: yield Service( - item=port, + item=port.port_id, parameters={ - 'enabled': section[port].enabled, - 'status': section[port].status, - 'speed': section[port].speed, + 'enabled': port.enabled, + 'status': port.status, + 'speed': port.speed, } ) +def render_network_bandwidth_bits(value: int) -> str: + return render.networkbandwidth(value/8) + + def check_switch_ports_statuses(item: str, params: Mapping[str, any], section: Mapping[str, SwitchPort]) -> CheckResult: def _status_changed(is_state: str, was_state: str, state: int, message: str): if is_state != was_state: @@ -263,9 +278,7 @@ def check_switch_ports_statuses(item: str, params: Mapping[str, any], section: M was_state = was_state if was_state else 'N/A' yield Result(state=State(state), notice=f'{message}: from {was_state}, to {is_state}') - try: - port: SwitchPort = section[item] - except KeyError: + if (port := section.get(item)) is None: return # check admin state changed @@ -286,7 +299,7 @@ def check_switch_ports_statuses(item: str, params: Mapping[str, any], section: M state=params['state_op_change'] ) if port.status.lower() == 'connected': # check operational state - yield Result(state=State.OK, notice=f'Operational status: {port.status}') + yield Result(state=State.OK, summary=f'({port.status})', details=f'Operational status: {port.status}') # check speed changed yield from _status_changed( is_state=port.speed, @@ -296,34 +309,43 @@ def check_switch_ports_statuses(item: str, params: Mapping[str, any], section: M ) if params['speed'] == port.speed: # only if speed unchanged yield Result(state=State.OK, summary=f'Speed: {port.speed}') - if port.duplex.lower() == 'full': # check duplex state - yield Result(state=State.OK, summary=f'Duplex: {port.duplex}') - else: - yield Result(state=State(params['state_not_full_duplex']), summary=f'Duplex: {port.duplex}') - yield Result(state=State.OK, summary=f'Clients: {port.client_count}') if params.get('show_traffic'): - yield from check_levels( - value=port.traffic.sent, # Bits - label='Out', - metric_name='if_out_bps', - render_func=lambda v: render.networkbandwidth(v/8), # Bytes - # notice_only=True, - ) yield from check_levels( value=port.traffic.recv, # Bits label='In', metric_name='if_in_bps', - render_func=lambda v: render.networkbandwidth(v/8), # Bytes + render_func=render_network_bandwidth_bits, # Bytes + # notice_only=True, + ) + yield from check_levels( + value=port.traffic.sent, # Bits + label='Out', + metric_name='if_out_bps', + render_func=render_network_bandwidth_bits, # Bytes # notice_only=True, ) + + if port.duplex.lower() == 'full': # check duplex state + yield Result(state=State.OK, notice=f'Duplex: {port.duplex}') + else: + yield Result(state=State(params['state_not_full_duplex']), notice=f'Duplex: {port.duplex}') + yield Result(state=State.OK, notice=f'Clients: {port.client_count}') else: - yield Result(state=State(params['state_not_connected']), summary=f'Operational status: {port.status}') + yield Result( + state=State(params['state_not_connected']), + summary=f'({port.status})', + details=f'Operational status: {port.status}' + ) else: - yield Result(state=State(params['state_disabled']), summary=f'Admin status: {_admin_status[port.enabled]}') + yield Result( + state=State(params['state_disabled']), + summary=f'({_admin_status[port.enabled].title()})', + details=f'Admin status: {_admin_status[port.enabled].title()}', + ) if port.is_up_link: - yield Result(state=State.OK, summary=f'UP-Link: {_is_up_link[port.is_up_link]}') + yield Result(state=State.OK, summary='UP-Link', details=f'UP-Link: {_is_up_link[port.is_up_link]}') else: yield Result(state=State.OK, notice=f'UP-Link: {_is_up_link[port.is_up_link]}') @@ -344,7 +366,7 @@ def check_switch_ports_statuses(item: str, params: Mapping[str, any], section: M yield Result(state=State.UNKNOWN, summary=f'Secure Port enabled', details=f'Secure Port: {port.secure_port}') -register.check_plugin( +check_plugin_cisco_meraki_org_switch_ports_statuses = CheckPlugin( name='cisco_meraki_org_switch_ports_statuses', service_name='Port %s', discovery_function=discover_switch_ports_statuses, @@ -360,7 +382,7 @@ register.check_plugin( check_ruleset_name='cisco_meraki_switch_ports_statuses', discovery_ruleset_name='discovery_cisco_meraki_switch_ports_statuses', discovery_default_parameters={ - 'discovered_port_states': [True, False, 'Connected', 'Disconnected'] + 'discovered_port_states': ['admin_enabled', 'admin_disabled', 'connected', 'disconnected'] } ) @@ -388,7 +410,7 @@ def inventory_meraki_cdp_cache(section: Mapping[str, SwitchPort]) -> InventoryRe ) -register.inventory_plugin( +inventory_plugin_inv_meraki_cdp_cache = InventoryPlugin( name='inv_meraki_cdp_cache', sections=['cisco_meraki_org_switch_ports_statuses'], inventory_function=inventory_meraki_cdp_cache, @@ -419,7 +441,7 @@ def inventory_meraki_lldp_cache(section: Mapping[str, SwitchPort]) -> InventoryR ) -register.inventory_plugin( +inventory_plugin_inv_meraki_lldp_cache = InventoryPlugin( name='inv_meraki_lldp_cache', sections=['cisco_meraki_org_switch_ports_statuses'], inventory_function=inventory_meraki_lldp_cache, diff --git a/source/agent_based/cisco_meraki_org_wireless_device_status.py b/source/cmk_addons_plugins/meraki/agent_based/wireless_device_ssid_status.py similarity index 88% rename from source/agent_based/cisco_meraki_org_wireless_device_status.py rename to source/cmk_addons_plugins/meraki/agent_based/wireless_device_ssid_status.py index 7542628..16b0345 100644 --- a/source/agent_based/cisco_meraki_org_wireless_device_status.py +++ b/source/cmk_addons_plugins/meraki/agent_based/wireless_device_ssid_status.py @@ -11,27 +11,24 @@ # 2024-04-27: made data parsing more robust # 2024-06-23: fixed crash on empty json_data # moved data parsing in to SSID class +# 2024-06-30: renamed from cisco_meraki_org_wireless_device_status.py int to wireless_device_ssid_status.py -from _collections_abc import Mapping +from collections.abc import Mapping from dataclasses import dataclass -from cmk.base.plugins.agent_based.agent_based_api.v1 import ( +from cmk.agent_based.v2 import ( + AgentSection, + CheckPlugin, + CheckResult, + DiscoveryResult, Metric, Result, Service, State, - register, -) -from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import ( - CheckResult, - DiscoveryResult, StringTable, ) -from cmk.base.plugins.agent_based.utils.cisco_meraki import ( - get_int, # type: ignore[import] - load_json, -) +from cmk_addons.plugins.meraki.lib.utils import get_int, load_json @dataclass(frozen=True) @@ -83,7 +80,7 @@ def parse_wireless_device_status(string_table: StringTable) -> Mapping[str, SSID return ssids -register.agent_section( +agent_section_cisco_meraki_org_wireless_device_status = AgentSection( name="cisco_meraki_org_wireless_device_status", parse_function=parse_wireless_device_status, ) @@ -104,10 +101,8 @@ def check_wireless_device_status( params: Mapping[str, any], section: Mapping[str, SSID] ) -> CheckResult: - try: - ssid: SSID = section[item] - except KeyError: - return None + if (ssid := section.get(item)) is None: + return yield Result(state=State.OK, summary=f'Name: {ssid.name}') if not ssid.enabled: @@ -123,12 +118,12 @@ def check_wireless_device_status( yield Result(state=State.OK, summary=f'Channel: {ssid.channel}') yield Metric(name='channel', value=ssid.channel) yield Result(state=State.OK, summary=f'Channel width: {ssid.channel_width}') - yield Metric(name='channel_width', value=int(ssid.channel_width.split(' ')[0]) * 1000000) # change to Hz from MHz + yield Metric(name='channel_width', value=int(ssid.channel_width.split(' ')[0]) * 1000000) # change MHz -> Hz yield Result(state=State.OK, summary=f'Power: {ssid.power}') yield Metric(name='signal_power', value=int(ssid.power.split(' ')[0])) -register.check_plugin( +check_plugin_cisco_meraki_org_wireless_device_status = CheckPlugin( name='cisco_meraki_org_wireless_device_status', service_name='SSID %s', discovery_function=discover_wireless_device_status, @@ -138,4 +133,3 @@ register.check_plugin( }, check_ruleset_name='cisco_meraki_wireless_device_status', ) - diff --git a/source/agent_based/cisco_meraki_org_wireless_ethernet_statuses.py b/source/cmk_addons_plugins/meraki/agent_based/wireless_ethernet_statuses.py similarity index 75% rename from source/agent_based/cisco_meraki_org_wireless_ethernet_statuses.py rename to source/cmk_addons_plugins/meraki/agent_based/wireless_ethernet_statuses.py index 046fd9a..9fc2fca 100644 --- a/source/agent_based/cisco_meraki_org_wireless_ethernet_statuses.py +++ b/source/cmk_addons_plugins/meraki/agent_based/wireless_ethernet_statuses.py @@ -9,26 +9,27 @@ # File : cisco_meraki_org_wireless_ethernet_statuses.py (check plugin) # 2024-04-27: made data parsing more robust +# 2024-06-29: refactored for CMK 2.3 +# moved parse functions to class methods +# 2024-06-30: renamed from cisco_meraki_org_wireless_ethernet_statuses.py in to wireless_ethernet_statuses.py +# ToDo: create ruleset cisco_meraki_wireless_ethernet_statuses + +from collections.abc import Mapping from dataclasses import dataclass -from _collections_abc import Mapping -from cmk.base.plugins.agent_based.agent_based_api.v1 import ( +from cmk.agent_based.v2 import ( + AgentSection, + CheckPlugin, + CheckResult, + DiscoveryResult, Result, Service, State, - register, - render, -) -from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import ( - CheckResult, - DiscoveryResult, StringTable, + render, ) -from cmk.base.plugins.agent_based.utils.cisco_meraki import ( - get_int, # type: ignore[import] - load_json, -) +from cmk_addons.plugins.meraki.lib.utils import get_int, load_json __ethernet_port_statuses = { 'aggregation': { @@ -70,6 +71,14 @@ class WirelessEthernetPortPower: ac: bool | None poe: bool | None + @classmethod + def parse(cls, power: Mapping[str, object]): + return cls( + mode=str(power['mode']) if power.get('mode') is not None else None, + ac=bool(power['ac']['isConnected']) if power.get('ac', {}).get('isConnected') is not None else None, + poe=bool(power['poe']['isConnected']) if power.get('poe', {}).get('isConnected') is not None else None, + ) + @dataclass(frozen=True) class WirelessEthernetPort: @@ -79,6 +88,19 @@ class WirelessEthernetPort: speed: int | None power: WirelessEthernetPortPower | None + @classmethod + def parse(cls, port: Mapping[str, object], power: WirelessEthernetPortPower): + return cls( + name=str(port['name']) if port.get('name') is not None else None, + power=power, + duplex=port['linkNegotiation']['duplex'] if port.get('linkNegotiation', {}).get( + 'duplex') is not None else None, + # changed to bit/s + speed=int(port['linkNegotiation']['speed']) * 125000 if get_int(port.get('linkNegotiation', {}).get( + 'speed')) else None, + poe=str(port['poe']['standard']) if port.get('poe', {}).get('standard') is not None else None, + ) + _is_connected = { True: 'Yes', @@ -90,29 +112,12 @@ def parse_wireless_ethernet_statuses(string_table: StringTable) -> Mapping[str, json_data = load_json(string_table) json_data = json_data[0] - power = WirelessEthernetPortPower( - mode=str(json_data['power']['mode']) if json_data['power'].get('mode') is not None else None, - ac=bool(json_data['power']['ac']['isConnected']) if json_data['power'].get('ac', {}).get( - 'isConnected') is not None else None, - poe=bool(json_data['power']['poe']['isConnected']) if json_data['power'].get('poe', {}).get( - 'isConnected') is not None else None, - ) if json_data.get('power') is not None else None + power = WirelessEthernetPortPower.parse(json_data['power']) if json_data.get('power') is not None else None - return { - port['name']: WirelessEthernetPort( - name=str(port['name']) if port.get('name') is not None else None, - power=power, - duplex=port['linkNegotiation']['duplex'] if port.get('linkNegotiation', {}).get( - 'duplex') is not None else None, - # changed to bit/s - speed=int(port['linkNegotiation']['speed']) * 125000 if get_int(port.get('linkNegotiation', {}).get( - 'speed')) else None, - poe=str(port['poe']['standard']) if port.get('poe', {}).get('standard') is not None else None, - ) for port in json_data.get('ports', []) - } + return {port['name']: WirelessEthernetPort.parse(port, power) for port in json_data.get('ports', [])} -register.agent_section( +agent_section_cisco_meraki_org_wireless_ethernet_statuses = AgentSection( name="cisco_meraki_org_wireless_ethernet_statuses", parse_function=parse_wireless_ethernet_statuses, ) @@ -132,9 +137,7 @@ def check_wireless_ethernet_statuses( if is_state != was_state: yield Result(state=State(state), notice=f'{message}: is {is_state}, was {was_state}') - try: - port: WirelessEthernetPort = section[item] - except KeyError: + if (port := section.get(item)) is None: return None if port.speed: @@ -171,7 +174,7 @@ def check_wireless_ethernet_statuses( yield Result(state=State.OK, summary=f'PoE standard: {port.poe}') -register.check_plugin( +check_plugin_cisco_meraki_org_wireless_ethernet_statuses = CheckPlugin( name='cisco_meraki_org_wireless_ethernet_statuses', service_name='Port %s', discovery_function=discover_wireless_ethernet_statuses, @@ -182,5 +185,5 @@ register.check_plugin( 'state_no_speed': 1, 'state_speed_change': 1, }, - check_ruleset_name='cisco_meraki_wireless_ethernet_statuses', + # check_ruleset_name='cisco_meraki_wireless_ethernet_statuses', ) diff --git a/source/cmk_addons_plugins/meraki/graphing/packages.py b/source/cmk_addons_plugins/meraki/graphing/packages.py new file mode 100644 index 0000000..108400b --- /dev/null +++ b/source/cmk_addons_plugins/meraki/graphing/packages.py @@ -0,0 +1,238 @@ +#!/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 : 2023-11-04 +# File : cisco_meraki.py (metrics) +# +# 2023-11-12: added wireless device status (channel, channel width, signal power) +# 2024-05-12: added switch port statuses and API return Codes +# 2024-06-24: fixed, wrong total for SSID perfometer signal_power -> 30 +# 2024-06-27: refactored for CMK 2.3 +# 2024-06-30: renamed from cisco_meraki.py in to packages.py + +from cmk.graphing.v1 import Title, graphs, metrics, perfometers + +# +# unit definitions +# +UNIT_DBM = metrics.Unit(metrics.DecimalNotation('dBm')) +UNIT_HZ = metrics.Unit(metrics.SINotation('hZ')) +UNIT_NUMBER = metrics.Unit(metrics.DecimalNotation('')) +UNIT_PERCENT = metrics.Unit(metrics.DecimalNotation('%')) +UNIT_TIME = metrics.Unit(metrics.TimeNotation()) +# +# license overview +# +metric_sum_licensed_devices = metrics.Metric( + name='sum_licensed_devices', + title=Title('Licensed devices'), + unit=UNIT_NUMBER, + color=metrics.Color.LIGHT_GREEN, +) + +metric_remaining_time = metrics.Metric( + name='remaining_time', + title=Title('Remaining time'), + unit=UNIT_TIME, + color=metrics.Color.GREEN, +) + +graph_cisco_meraki_remaining_time = graphs.Graph( + name='cisco_meraki.remaining_time', + title=Title('Cisco Meraki Licenses remaining time'), + compound_lines=['remaining_time'], + simple_lines=[ + metrics.WarningOf("remaining_time"), + metrics.CriticalOf("remaining_time"), + ], + minimal_range=graphs.MinimalRange(0, 180), +) + +graph_cisco_meraki_licensed_devices = graphs.Graph( + name='cisco_meraki_licensed_devices', + title=Title('Cisco Meraki Licensed devices'), + compound_lines=['sum_licensed_devices'], + minimal_range=graphs.MinimalRange(0, 10), +) + +perfometer_licensing = perfometers.Stacked( + name="merak_licensing", + # upper and lower are in the wrong order + lower=perfometers.Perfometer( + name='sum_licensed_devices', + focus_range=perfometers.FocusRange(perfometers.Open(0), perfometers.Open(100)), + segments=['sum_licensed_devices'], + ), + upper=perfometers.Perfometer( + name='remaining_time', + focus_range=perfometers.FocusRange(perfometers.Open(0), perfometers.Open(180)), + segments=["remaining_time"], + ), +) + +# +# wireless devices status +# +metric_signal_power = metrics.Metric( + name='signal_power', + title=Title("Power"), + unit=UNIT_DBM, + color=metrics.Color.GREEN, +) + +metric_channel_width = metrics.Metric( + name="channel_width", + title=Title("Channel Width"), + unit=UNIT_HZ, + color=metrics.Color.BLUE, +) + +metric_channel = metrics.Metric( + name="channel", + title=Title("Channel"), + unit=UNIT_NUMBER, + color=metrics.Color.DARK_YELLOW, +) + +graph_cisco_meraki_wireless_device_status_signal_power = graphs.Graph( + name='cisco_meraki_wireless_device_status_signal_power', + title=Title('Signal power'), + compound_lines=['signal_power'], + minimal_range=graphs.MinimalRange(0, 'signal_power:max'), +) + +graph_cisco_meraki_wireless_device_status_channel_width = graphs.Graph( + name='cisco_meraki_wireless_device_status_channel_width', + title=Title('Channel Width'), + compound_lines=['channel_width'], + minimal_range=graphs.MinimalRange(0, 'channel_width:max'), +) + +graph_cisco_meraki_wireless_device_status_channel = graphs.Graph( + name='cisco_meraki_wireless_device_status_channel', + title=Title('Channel'), + compound_lines=['channel'], + minimal_range=graphs.MinimalRange(0, 'channel:max'), +) + +perfometer_signal_power = perfometers.Perfometer( + name='signal_power', + segments=['signal_power'], + focus_range=perfometers.FocusRange(perfometers.Closed(0), perfometers.Closed(40)) +) + +# +# API return Codes +# +metric_api_code_2xx = metrics.Metric( + name="api_code_2xx", + title=Title("Code 2xx"), + unit=UNIT_NUMBER, + color=metrics.Color.LIGHT_GREEN, +) + +metric_api_code_3xx = metrics.Metric( + name="api_code_3xx", + title=Title("Code 3xx"), + unit=UNIT_NUMBER, + color=metrics.Color.LIGHT_BLUE, +) + +metric_api_code_4xx = metrics.Metric( + name="api_code_4xx", + title=Title("Code 4xx"), + unit=UNIT_NUMBER, + color=metrics.Color.LIGHT_RED, +) + +metric_api_code_5xx = metrics.Metric( + name="api_code_5xx", + title=Title("Code 5xx"), + unit=UNIT_NUMBER, + color=metrics.Color.DARK_RED, +) + +graph_cisco_meraki_cisco_meraki_organisations_api_code = graphs.Bidirectional( + name='cisco_meraki_cisco_meraki_organisations_api_code', + title=Title('Cisco Meraki API response codes'), + upper=graphs.Graph( + name='api_code_ok', + title=Title('Cisco Meraki API response codes'), + simple_lines=[ + 'api_code_2xx', + 'api_code_3xx', + ], + optional=[ + 'api_code_2xx', + 'api_code_3xx', + ] + ), + lower=graphs.Graph( + name='api_codebad', + title=Title('Cisco Meraki API response codes'), + simple_lines=[ + 'api_code_4xx', + 'api_code_5xx', + ], + optional=[ + 'api_code_4xx', + 'api_code_5xx', + ] + ), +) + +perfometer_api_code = perfometers.Stacked( + name='api_code', + lower=perfometers.Perfometer( + name='api_code_2xx', + focus_range=perfometers.FocusRange(perfometers.Closed(0), perfometers.Open(30)), + segments=["api_code_2xx"], + ), + upper=perfometers.Perfometer( + name='api_code_4xx', + focus_range=perfometers.FocusRange(perfometers.Closed(0), perfometers.Open(30)), + segments=["api_code_4xx"], + ) +) + +perfometer_api_code_2xx = perfometers.Perfometer( + name='api_code_2xx', + segments=["api_code_2xx"], + focus_range=perfometers.FocusRange(perfometers.Closed(0), perfometers.Open(50)), +) + +perfometer_api_code_4xx = perfometers.Perfometer( + name='api_code_4xx', + segments=['api_code_4xx'], + focus_range=perfometers.FocusRange(perfometers.Closed(0), perfometers.Open(50)), +) + +# +# appliance performance/utilization +# +metric_utilization = metrics.Metric( + name="utilization", + title=Title("Utilization"), + unit=UNIT_PERCENT, + color=metrics.Color.LIGHT_GREEN, +) + +graph_cisco_meraki_cisco_meraki_appliance_utilization = graphs.Graph( + name='cisco_meraki_cisco_meraki_appliance_utilization', + title=Title('Cisco Meraki Appliance Utilization'), + compound_lines=['utilization'], + simple_lines=[ + metrics.WarningOf("utilization"), + metrics.CriticalOf("utilization"), + ], + minimal_range=graphs.MinimalRange(0, 100), +) + +perfometer_utilization = perfometers.Perfometer( + name='utilization', + segments=['utilization'], + focus_range=perfometers.FocusRange(perfometers.Closed(0), perfometers.Closed(100)), +) diff --git a/source/lib/python3/cmk/special_agents/agent_cisco_meraki.py b/source/cmk_addons_plugins/meraki/lib/agent.py similarity index 95% rename from source/lib/python3/cmk/special_agents/agent_cisco_meraki.py rename to source/cmk_addons_plugins/meraki/lib/agent.py index e1bd3fc..860cc46 100644 --- a/source/lib/python3/cmk/special_agents/agent_cisco_meraki.py +++ b/source/cmk_addons_plugins/meraki/lib/agent.py @@ -37,8 +37,6 @@ # 2024-05-20: made appliance uplinks usage user selectable # made API requests per org user selectable # 2024-06-23: added cache time per section -> not nice but should work. -# 2024-06-24: renamed cache time per section option form --cache_per_section to --cache-per-section -# fixed --cache-per-section parameter evaluation # ToDo: create inventory from Networks, is per organisation, not sure where/how to put this in the inventory # ToDo: list Connected Datacenters like Umbrella https://developer.cisco.com/meraki/api-v1/list-data-centers/ @@ -64,45 +62,66 @@ from enum import auto, Enum from logging import getLogger from os import environ from pathlib import Path -from random import randrange +# from random import randrange from requests import request, RequestException from time import strftime, gmtime, time as now_time from time import time_ns -from typing import Final, TypedDict, Any +from typing import Final, TypedDict, Any, List # from urllib.request import getproxies import meraki # type: ignore[import] from cmk.utils.paths import tmp_dir -from cmk.special_agents.utils.agent_common import ( +from cmk.special_agents.v0_unstable.agent_common import ( ConditionalPiggybackSection, SectionWriter, special_agent_main, ) -from cmk.special_agents.utils.argument_parsing import create_default_argument_parser # , Args -from cmk.special_agents.utils.misc import DataCache - -from cmk.base.plugins.agent_based.utils.cisco_meraki import ( - MerakiNetwork, # type: ignore[import] - _SEC_NAME_APPLIANCE_UPLINKS, # type: ignore[import] - _SEC_NAME_APPLIANCE_UPLINKS_USAGE, # type: ignore[import] - _SEC_NAME_APPLIANCE_VPNS, # type: ignore[import] - _SEC_NAME_APPLIANCE_PERFORMANCE, # type: ignore[import] - _SEC_NAME_CELLULAR_UPLINKS, # type: ignore[import] - _SEC_NAME_DEVICE_INFO, # type: ignore[import] - _SEC_NAME_DEVICE_STATUSES, # type: ignore[import] - _SEC_NAME_DEVICE_UPLINKS_INFO, # type: ignore[import] - _SEC_NAME_LICENSES_OVERVIEW, # type: ignore[import] - _SEC_NAME_NETWORKS, # type: ignore[import] - _SEC_NAME_ORGANISATIONS, # type: ignore[import] - _SEC_NAME_ORG_API_REQUESTS, # type: ignore[import] - _SEC_NAME_SENSOR_READINGS, # type: ignore[import] - _SEC_NAME_SWITCH_PORTS_STATUSES, # type: ignore[import] - _SEC_NAME_WIRELESS_DEVICE_STATUS, # type: ignore[import] - _SEC_NAME_WIRELESS_ETHERNET_STATUSES, # type: ignore[import] +from cmk.special_agents.v0_unstable.argument_parsing import create_default_argument_parser # , Args +from cmk.special_agents.v0_unstable.misc import DataCache + +from cmk_addons.plugins.meraki.lib.utils import ( + MerakiNetwork, + + # parameter names + _SEC_NAME_APPLIANCE_UPLINKS, + _SEC_NAME_APPLIANCE_UPLINKS_USAGE, + _SEC_NAME_APPLIANCE_VPNS, + _SEC_NAME_APPLIANCE_PERFORMANCE, + _SEC_NAME_CELLULAR_UPLINKS, + _SEC_NAME_DEVICE_INFO, + _SEC_NAME_DEVICE_STATUSES, + _SEC_NAME_DEVICE_UPLINKS_INFO, + _SEC_NAME_LICENSES_OVERVIEW, + _SEC_NAME_NETWORKS, + _SEC_NAME_ORGANISATIONS, + _SEC_NAME_ORG_API_REQUESTS, + _SEC_NAME_SENSOR_READINGS, + _SEC_NAME_SWITCH_PORTS_STATUSES, + _SEC_NAME_WIRELESS_DEVICE_STATUS, + _SEC_NAME_WIRELESS_ETHERNET_STATUSES, # Early Access - _SEC_NAME_ORG_SWITCH_PORTS_STATUSES, # type: ignore[import] + _SEC_NAME_ORG_SWITCH_PORTS_STATUSES, + + # api cache defaults per section + _SEC_CACHE_APPLIANCE_PERFORMANCE, + _SEC_CACHE_APPLIANCE_UPLINKS_USAGE, + _SEC_CACHE_APPLIANCE_UPLINKS, + _SEC_CACHE_APPLIANCE_VPNS, + _SEC_CACHE_CELLULAR_UPLINKS, + _SEC_CACHE_DEVICE_INFO, + _SEC_CACHE_DEVICE_STATUSES, + _SEC_CACHE_DEVICE_UPLINKS_INFO, + _SEC_CACHE_LICENSES_OVERVIEW, + _SEC_CACHE_NETWORKS, + _SEC_CACHE_ORG_API_REQUESTS, + _SEC_CACHE_ORG_SWITCH_PORTS_STATUSES, + _SEC_CACHE_ORGANISATIONS, + _SEC_CACHE_SENSOR_READINGS, + _SEC_CACHE_SWITCH_PORTS_STATUSES, + _SEC_CACHE_WIRELESS_DEVICE_STATUS, + _SEC_CACHE_WIRELESS_ETHERNET_STATUSES, ) _LOGGER = getLogger("agent_cisco_meraki") @@ -122,6 +141,7 @@ _API_NAME_NETWORK_ID: Final = 'networkId' _API_NAME_ORGANISATION_ID: Final = "id" _API_NAME_ORGANISATION_NAME: Final = "name" +# map section parameter name to python name (do we really need this, why not use the name ("-" -> "_")? _SECTION_NAME_MAP = { _SEC_NAME_APPLIANCE_UPLINKS: "appliance_uplinks", _SEC_NAME_APPLIANCE_UPLINKS_USAGE: "appliance_uplinks_usage", @@ -845,7 +865,6 @@ class MerakiOrganisation: piggyback=self._adjust_piggyback(host=piggyback), ) - if devices_by_type.get(_API_NAME_DEVICE_TYPE_SWITCH): if _SEC_NAME_SWITCH_PORTS_STATUSES not in self.config.excluded_sections: for switch in devices_by_type[_API_NAME_DEVICE_TYPE_SWITCH]: @@ -1102,7 +1121,7 @@ def parse_arguments(argv: Sequence[str] | None) -> Args: parser.add_argument( "--excluded-sections", - nargs="+", + nargs="*", choices=list(_SECTION_NAME_MAP), default=[], help="Sections that are excluded form data collected.", @@ -1144,8 +1163,26 @@ def parse_arguments(argv: Sequence[str] | None) -> Args: '--cache-per-section', nargs="+", type=int, - help="List of chache time per section in minutes", - default=[0, 0, 60, 60, 60, 60, 60, 60, 600, 600, 0, 0, 600, 0, 0, 30, 30] + help="List of cache time per section in minutes", + default=[ + _SEC_CACHE_APPLIANCE_PERFORMANCE, + _SEC_CACHE_APPLIANCE_UPLINKS_USAGE, + _SEC_CACHE_APPLIANCE_UPLINKS, + _SEC_CACHE_APPLIANCE_VPNS, + _SEC_CACHE_CELLULAR_UPLINKS, + _SEC_CACHE_DEVICE_INFO, + _SEC_CACHE_DEVICE_STATUSES, + _SEC_CACHE_DEVICE_UPLINKS_INFO, + _SEC_CACHE_LICENSES_OVERVIEW, + _SEC_CACHE_NETWORKS, + _SEC_CACHE_ORG_API_REQUESTS, + _SEC_CACHE_ORG_SWITCH_PORTS_STATUSES, + _SEC_CACHE_ORGANISATIONS, + _SEC_CACHE_SENSOR_READINGS, + _SEC_CACHE_SWITCH_PORTS_STATUSES, + _SEC_CACHE_WIRELESS_DEVICE_STATUS, + _SEC_CACHE_WIRELESS_ETHERNET_STATUSES + ] ) return parser.parse_args(argv) diff --git a/source/agent_based/utils/cisco_meraki.py b/source/cmk_addons_plugins/meraki/lib/utils.py similarity index 65% rename from source/agent_based/utils/cisco_meraki.py rename to source/cmk_addons_plugins/meraki/lib/utils.py index 2ada91f..810af86 100644 --- a/source/agent_based/utils/cisco_meraki.py +++ b/source/cmk_addons_plugins/meraki/lib/utils.py @@ -8,6 +8,8 @@ # - changed check_last_reported_ts to output report as metric/levels # - added levels_upper check_last_reported_ts # - added section names from the cisco meraki special agent (for use in WATO) +# 2024-06-30: moved to cmk_addons/plugins/meraki/lib +# renamed from cisco_meraki.py to utils.py import json import time @@ -20,6 +22,7 @@ from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import CheckResul MerakiAPIData = Mapping[str, object] +# parameter names for agent options _SEC_NAME_ORGANISATIONS: Final = "_organisations" # internal use runs always _SEC_NAME_DEVICE_INFO: Final = "_device_info" # Not configurable, needed for piggyback _SEC_NAME_NETWORKS: Final = "_networks" # internal use, runs always, needed for network names @@ -30,7 +33,7 @@ _SEC_NAME_APPLIANCE_UPLINKS_USAGE: Final = "appliance-uplinks-usage" _SEC_NAME_APPLIANCE_VPNS: Final = "appliance-vpns" _SEC_NAME_APPLIANCE_PERFORMANCE: Final = "appliance-performance" _SEC_NAME_CELLULAR_UPLINKS: Final = "cellular-uplinks" -_SEC_NAME_DEVICE_STATUSES: Final = "device-statuses" +_SEC_NAME_DEVICE_STATUSES: Final = "device-status" _SEC_NAME_DEVICE_UPLINKS_INFO: Final = "device-uplinks-info" _SEC_NAME_LICENSES_OVERVIEW: Final = "licenses-overview" _SEC_NAME_SENSOR_READINGS: Final = "sensor-readings" @@ -39,6 +42,27 @@ _SEC_NAME_WIRELESS_DEVICE_STATUS: Final = "wireless-device-status" _SEC_NAME_WIRELESS_ETHERNET_STATUSES: Final = "wireless-ethernet-statuses" +# api cache defaults per section +_SEC_CACHE_APPLIANCE_PERFORMANCE = 0 +_SEC_CACHE_APPLIANCE_UPLINKS_USAGE = 0 +_SEC_CACHE_APPLIANCE_UPLINKS = 60 +_SEC_CACHE_APPLIANCE_VPNS = 60 +_SEC_CACHE_CELLULAR_UPLINKS = 60 +_SEC_CACHE_DEVICE_INFO = 60 +_SEC_CACHE_DEVICE_STATUSES = 60 +_SEC_CACHE_DEVICE_UPLINKS_INFO = 60 +_SEC_CACHE_LICENSES_OVERVIEW = 600 +_SEC_CACHE_NETWORKS = 600 +_SEC_CACHE_ORG_API_REQUESTS = 0 +_SEC_CACHE_ORG_SWITCH_PORTS_STATUSES = 0 +_SEC_CACHE_ORGANISATIONS = 600 +_SEC_CACHE_SENSOR_READINGS = 0 +_SEC_CACHE_SWITCH_PORTS_STATUSES = 0 +_SEC_CACHE_WIRELESS_DEVICE_STATUS = 30 +_SEC_CACHE_WIRELESS_ETHERNET_STATUSES = 30 + + + # Early Access _SEC_NAME_ORG_SWITCH_PORTS_STATUSES: Final = "org-switch-ports-statuses" @@ -100,3 +124,33 @@ def get_float(value: str | None) -> float | None: return float(value) except TypeError: return + + +def add_org_id_name_to_output( + organisation_id: str, + organisation_name: str, + item_variant: str, + dont_show_alias_on_info: bool, +) -> GeneratorExit: + org_id = f'[{organisation_id}]' + org_name = f'[{organisation_name}]' + org_id_notice = f'Organisation ID: {organisation_id}' + org_name_notice = f'Organisation name: {organisation_name}' + + match item_variant: + case 'org_id': + yield Result(state=State.OK, notice=org_id_notice) + if dont_show_alias_on_info: + yield Result(state=State.OK, notice=org_name_notice) + else: + yield Result(state=State.OK, summary=org_name, details=org_name_notice) + case 'org_name': + if dont_show_alias_on_info: + yield Result(state=State.OK, notice=org_id_notice) + else: + yield Result(state=State.OK, summary=org_id, details=org_id_notice) + yield Result(state=State.OK, notice=org_name_notice) + + case _: + yield Result(state=State.OK, notice=org_id_notice) + yield Result(state=State.OK, notice=org_name_notice) \ No newline at end of file diff --git a/source/cmk_addons_plugins/meraki/rulesets/appliance_performance.py b/source/cmk_addons_plugins/meraki/rulesets/appliance_performance.py new file mode 100644 index 0000000..5424f4f --- /dev/null +++ b/source/cmk_addons_plugins/meraki/rulesets/appliance_performance.py @@ -0,0 +1,63 @@ +#!/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 : 2024-06-23 +# File : cisco_meraki_org_appliance_performance.py (WATO) + +# 2024-06-27: refactored for CMK 2.3 +# 2024-06-30: renamed from cisco_meraki_org_appliance_performance.py in to appliance_performance.py +from cmk.rulesets.v1 import Help, Title +from cmk.rulesets.v1.form_specs import ( + DefaultValue, + DictElement, + Dictionary, + Integer, + LevelDirection, + SimpleLevels, +) +from cmk.rulesets.v1.rule_specs import CheckParameters, HostCondition, Topic + + +def _parameter_form() -> Dictionary: + return Dictionary( + elements={ + 'levels_upper': DictElement( + parameter_form=SimpleLevels( + title=Title("Utilization"), + level_direction=LevelDirection.UPPER, + form_spec_template=Integer(), + prefill_fixed_levels=DefaultValue(value=(60, 80)), + help_text=Help( + 'The device utilization data reported to the Meraki' + ' dashboard is based on a load average measured over a' + ' period of one minute. The load value is returned in' + ' numeric values ranging from 1 through 100. A lower' + ' value indicates a lower load, and a higher value' + ' indicates a more intense workload. Currently, the' + ' device utilization value is calculated based upon the' + ' CPU utilization of the MX as well as its traffic load.' + ' If an MX device is consistently over 50% utilization' + ' during normal operation, upgrading to a higher' + ' throughput model or reducing the per-device load' + ' through horizontal scaling should be considered. For' + ' more information see:' + ' https://documentation.meraki.com-MX-Monitoring?and?' + 'Reporting-Device?Utiliyation' + ), + ) + ) + }, + ) + + +rule_spec_cisco_meraki_org_appliance_performance = CheckParameters( + name="cisco_meraki_org_appliance_performance", + topic=Topic.NETWORKING, + parameter_form=_parameter_form, + title=Title("Cisco Meraki Appliance Utilization"), + condition=HostCondition(), +) diff --git a/source/cmk_addons_plugins/meraki/rulesets/appliance_uplinks.py b/source/cmk_addons_plugins/meraki/rulesets/appliance_uplinks.py new file mode 100644 index 0000000..6129d37 --- /dev/null +++ b/source/cmk_addons_plugins/meraki/rulesets/appliance_uplinks.py @@ -0,0 +1,74 @@ +#!/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 : 2023-11-05 +# File : cisco_meraki_org_appliance_uplinks.py (WATO) + +# 2024-06-29: refactored for CMK 2.3 +# 2024-06-30: renamed from cisco_meraki_org_appliance_uplinks.py in to appliance_uplinks.py + +from cmk.rulesets.v1 import Label, Title, Help +from cmk.rulesets.v1.form_specs import ( + DefaultValue, + DictElement, + Dictionary, + FixedValue, + ServiceState, +) +from cmk.rulesets.v1.rule_specs import CheckParameters, HostAndItemCondition, Topic + + +def _parameter_form() -> Dictionary: + return Dictionary( + elements={ + 'status_map': DictElement( + parameter_form=Dictionary( + title=Title('Map uplink status to monitoring state'), + elements={ + "active": DictElement( + parameter_form=ServiceState( + title=Title('Uplink status "active"'), + prefill=DefaultValue(ServiceState.OK) + )), + "ready": DictElement( + parameter_form=ServiceState( + title=Title('Uplink status "ready"'), + prefill=DefaultValue(ServiceState.WARN), + )), + "not_connected": DictElement( + parameter_form=ServiceState( + title=Title('Uplink status "not connected"'), + prefill=DefaultValue(ServiceState.CRIT), + )), + "failed": DictElement( + parameter_form=ServiceState( + title=Title('Uplink status "failed"'), + prefill=DefaultValue(ServiceState.CRIT), + )), + }, + )), + 'show_traffic': DictElement( + parameter_form=FixedValue( + title=Title('Show bandwidth (use only with cache disabled)'), + help_text=Help( + 'Use only with cache disabled in the Meraki special agent settings. ' + 'The throughput is based on the usage for the last 60 seconds.' + ), + value=True, + label=Label("Bandwidth monitoring enabled") + )) + }, + ) + + +rule_spec_cisco_meraki_org_appliance_uplinks = CheckParameters( + name="cisco_meraki_org_appliance_uplinks", + topic=Topic.NETWORKING, + parameter_form=_parameter_form, + title=Title("Cisco Meraki Appliance uplinks"), + condition=HostAndItemCondition(item_title=Title('Uplink name')), +) diff --git a/source/cmk_addons_plugins/meraki/rulesets/appliance_vpns.py b/source/cmk_addons_plugins/meraki/rulesets/appliance_vpns.py new file mode 100644 index 0000000..3cc8cca --- /dev/null +++ b/source/cmk_addons_plugins/meraki/rulesets/appliance_vpns.py @@ -0,0 +1,42 @@ +#!/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 : 2023-11-05 +# File : cisco_meraki_org_appliance_vpns.py (WATO) + +# 2024-06-29: refactored for CMK 2.3 +# 2024-06-30: renamed from cisco_meraki_org_appliance_vpns.py in to appliance_vpns.py + +from cmk.rulesets.v1 import Title +from cmk.rulesets.v1.form_specs import ( + DefaultValue, + DictElement, + Dictionary, + ServiceState, +) +from cmk.rulesets.v1.rule_specs import CheckParameters, HostAndItemCondition, Topic + + +def _parameter_form() -> Dictionary: + return Dictionary( + elements={ + 'status_not_reachable': DictElement( + parameter_form=ServiceState( + title=Title('Monitoring state if the VPN peer is not reachable'), + prefill=DefaultValue(ServiceState.WARN), + )), + } + ) + + +rule_spec_cisco_meraki_org_appliance_vpns = CheckParameters( + name="cisco_meraki_org_appliance_vpns", + topic=Topic.NETWORKING, + parameter_form=_parameter_form, + title=Title("Cisco Meraki Appliance VPNs"), + condition=HostAndItemCondition(item_title=Title('VPN peer')), +) diff --git a/source/cmk_addons_plugins/meraki/rulesets/licenses_overviewi.py b/source/cmk_addons_plugins/meraki/rulesets/licenses_overviewi.py new file mode 100644 index 0000000..cd207a6 --- /dev/null +++ b/source/cmk_addons_plugins/meraki/rulesets/licenses_overviewi.py @@ -0,0 +1,100 @@ +#!/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. + +# enhancements by thl-cmk[at]outlook[dot]com, https://thl-cmk.hopto.org +# - changed remaining time (WATO) from Age (Days, Hours, Minutes, Seconds) to Days only +# - added WATO option for License state is not ok -> default to WARN +# - added discovery rule for ITEM variant (Org Name/Org ID - this is the default, Org Name, Org ID) + +# 2023-11-18: moved discovery rule set to cisco_meraki_organisations for reuse with cisco_meraki_organisations_api +# 2024-06-28: refactored for CMK 2.3 +# 2024-06-30: renamed from cisco_meraki_org_licenses_overviewi.py in to licenses_overviewi.py + +from cmk.rulesets.v1 import Label, Title, Help +from cmk.rulesets.v1.form_specs import ( + DefaultValue, + DictElement, + Dictionary, + FixedValue, + ServiceState, + SimpleLevels, + LevelDirection, + TimeSpan, + TimeMagnitude, + validators, +) +from cmk.rulesets.v1.rule_specs import CheckParameters, HostAndItemCondition, Topic + +_DAY = 60.0 * 60.0 * 24.0 + + +def _migrate(p: str | dict) -> dict[str, any]: + # change age to days, we suspect there is no warning less than 1 hour in seconds, + # and we will not warn/crit above 3600 days (10 years) + if p.get('remaining_expiration_time'): + warn, crit = p.get('remaining_expiration_time') + if warn >= 3600: + warn = int(warn / 86400) + if crit >= 3600: + crit = int(crit / 86400) + p['remaining_expiration_time'] = (warn, crit) + return p + + +def _parameter_form() -> Dictionary: + return Dictionary( + elements={ + "remaining_expiration_time": DictElement( + parameter_form=SimpleLevels[float]( + title=Title("Remaining licenses expiration time"), + help_text=Help(""), + form_spec_template=TimeSpan( + displayed_magnitudes=[TimeMagnitude.DAY], + custom_validate=(validators.NumberInRange(min_value=0),), + ), + level_direction=LevelDirection.LOWER, + prefill_fixed_levels=DefaultValue((40 * _DAY, 20 * _DAY)), + )), + # SimpleLevels[int]( + # title=Title("Lower levels for remaining expiration time of licenses"), + # level_direction=LevelDirection.LOWER, + # # elements=[ + # # Integer(title=_("Warning at"), unit='Days'), + # # Integer(title=_("Critical at"), unit='Days'), + # # ], + # )), + "state_license_not_ok": DictElement( + parameter_form=ServiceState( + title=Title('Monitoring state if License state is not OK'), + prefill=DefaultValue(ServiceState.WARN), + )), + 'dont_show_alias_on_info': DictElement( + parameter_form=FixedValue( + value=True, + title=Title('Don\'t show alias on info line'), + label=Label(''), + # help_test=Help( + # 'The alias is the Organisation ID or the Organisation name, depending on the Item.\n' + # 'If the item is the Organisation ID, the alias is the Organisation name and vice versa.\n' + # 'Organisation ID and Organisation name will always shown in the service details.' + # ) + )) + }, + # migrate=_migrate, + # ignored_keys=[ + # 'internal_item_name', + # 'old_item_name', + # 'item_variant', + # ], + ) + + +rule_spec_cisco_meraki_org_licenses_overview = CheckParameters( + name="cisco_meraki_org_licenses_overview", + topic=Topic.NETWORKING, + parameter_form=_parameter_form, + title=Title("Cisco Meraki Organisation Licenses Overview"), + condition=HostAndItemCondition(item_title=Title('Organization')), +) diff --git a/source/cmk_addons_plugins/meraki/rulesets/organisations.py b/source/cmk_addons_plugins/meraki/rulesets/organisations.py new file mode 100644 index 0000000..1161b45 --- /dev/null +++ b/source/cmk_addons_plugins/meraki/rulesets/organisations.py @@ -0,0 +1,62 @@ +#!/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 : 2023-11-11 +# File : cisco_meraki_organisations.py (wato plugin) + +# 2023-11-18: split from cisco_meraki_org_licenses_overview.py +# 2024-06-29: refactored for CMK 2.3 +# 2024-06-30: renamed from cisco_meraki_organisations.py in to organisations.py + +from cmk.rulesets.v1 import Help, Title +from cmk.rulesets.v1.form_specs import ( + DefaultValue, + DictElement, + Dictionary, + SingleChoice, + SingleChoiceElement, +) +from cmk.rulesets.v1.rule_specs import DiscoveryParameters, HostCondition, Topic + + +def _discovery_form() -> Dictionary: + return Dictionary( + elements={ + 'item_variant': DictElement( + parameter_form=SingleChoice( + title=Title('Information to use as item'), + help_text=Help( + 'You can select how to build the item for this service. By default the Organization ID/name\n' + 'is used to stay compatible with the build in check. The information not used for the item\n' + 'will be added to the service output.' + ), + elements=[ + SingleChoiceElement( + name='org_id', + title=Title('Organization ID') + ), + SingleChoiceElement( + name='org_name', + title=Title('Organization name') + ), + SingleChoiceElement( + name='org_id_name', + title=Title('Organization ID/name') + ), + ], + prefill=DefaultValue('org_id_name') + )) + } + ) + + +rule_spec_discovery_meraki_organisations = DiscoveryParameters( + name="discovery_meraki_organisations", + topic=Topic.GENERAL, + parameter_form=_discovery_form, + title=Title("Cisco Meraki Organisation (API/Licenses)"), +) diff --git a/source/cmk_addons_plugins/meraki/rulesets/organisations_api.py b/source/cmk_addons_plugins/meraki/rulesets/organisations_api.py new file mode 100644 index 0000000..e76cc02 --- /dev/null +++ b/source/cmk_addons_plugins/meraki/rulesets/organisations_api.py @@ -0,0 +1,62 @@ +#!/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 : 2023-11-18 +# File : cisco_meraki_organisations_api.py (wato plugin) + +# 2024-06-29: refactored for CMK 2.3 +# 2024-06-30: renamed from cisco_meraki_organisations_api.py in to organisations_api.py +# moved ruleset from "Networking" to "Applications, Processes & Services" + +from cmk.rulesets.v1 import Title +from cmk.rulesets.v1.form_specs import ( + DefaultValue, + DictElement, + Dictionary, + ServiceState, + String, +) +from cmk.rulesets.v1.rule_specs import CheckParameters, HostAndItemCondition, Topic + + +def _parameter_form() -> Dictionary: + return Dictionary( + elements={ + "state_api_not_enabled": DictElement( + parameter_form=ServiceState( + title=Title('Monitoring state if API is not enabled'), + prefill=DefaultValue(ServiceState.WARN), + )), + + # params from discovery + 'internal_item_name': DictElement( + render_only=True, + parameter_form=ServiceState( + title=Title('Discovery internal item name') + )), + 'item_variant': DictElement( + render_only=True, + parameter_form=ServiceState( + title=Title('Discovery item variant') + )) + }, + # ignored_keys=[ + # 'internal_item_name', + # 'old_item_name', + # 'item_variant', + # ], + ) + + +rule_spec_cisco_meraki_organisations_api = CheckParameters( + name="cisco_meraki_organisations_api", + topic=Topic.APPLICATIONS, + parameter_form=_parameter_form, + title=Title("Cisco Meraki Organisation API"), + condition=HostAndItemCondition(item_title=Title('Organization')), +) + diff --git a/source/cmk_addons_plugins/meraki/rulesets/switch_ports_statuses.py b/source/cmk_addons_plugins/meraki/rulesets/switch_ports_statuses.py new file mode 100644 index 0000000..3cea6f7 --- /dev/null +++ b/source/cmk_addons_plugins/meraki/rulesets/switch_ports_statuses.py @@ -0,0 +1,158 @@ +#!/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 : 2024-02-02 +# File : cisco_meraki_switch_ports_statuses.py (WATO) + +# 2024-05-12: added support for MerakiGetOrganizationSwitchPortsStatusesBySwitch (Early Access) +# added traffic counters as perfdata +# 2024-05-19: reworked switch port traffic +# 2024-05-20: added discovery rule for port status +# 2024-06-27: refactored for CMK 2.3 +# 2024-06-30: renamed from cisco_meraki_switch_ports_statuses.py in to switch_ports_statuses.py +# added params from discovery as render only + +from cmk.rulesets.v1 import Label, Title, Help +from cmk.rulesets.v1.form_specs import ( + DefaultValue, + DictElement, + Dictionary, + FixedValue, + ServiceState, + MultipleChoice, + MultipleChoiceElement, + String, +) +from cmk.rulesets.v1.rule_specs import CheckParameters, DiscoveryParameters, HostAndItemCondition, Topic + +from cmk_addons.plugins.meraki.lib.utils import ( + _SEC_NAME_ORG_SWITCH_PORTS_STATUSES, + _SEC_NAME_SWITCH_PORTS_STATUSES, +) + + +def _parameter_form(): + return Dictionary( + elements={ + 'state_disabled': DictElement( + parameter_form=ServiceState( + title=Title('Monitoring state if port is "disabled"'), + prefill=DefaultValue(ServiceState.OK), + )), + 'state_not_connected': DictElement( + parameter_form=ServiceState( + title=Title('Monitoring state if port is "not connected"'), + prefill=DefaultValue(ServiceState.OK), + )), + 'state_not_full_duplex': DictElement( + parameter_form=ServiceState( + title=Title('Monitoring state if port is "not full duplex"'), + prefill=DefaultValue(ServiceState.WARN), + )), + 'state_speed_change': DictElement( + parameter_form=ServiceState( + title=Title('Monitoring state if speed is changed'), + prefill=DefaultValue(ServiceState.WARN), + )), + 'state_admin_change': DictElement( + parameter_form=ServiceState( + title=Title('Monitoring state if admin state is changed'), + prefill=DefaultValue(ServiceState.WARN), + )), + 'state_op_change': DictElement( + parameter_form=ServiceState( + title=Title('Monitoring state if operational state is changed'), + prefill=DefaultValue(ServiceState.WARN), + )), + 'show_traffic': DictElement( + parameter_form=FixedValue( + value=True, + title=Title('Show bandwidth (use only with cache disabled)'), + label=Label('Bandwidth monitoring enabled'), + help_text=Help( + 'Use only with cache disabled in the Meraki special agent settings. ' + 'Depending on your Meraki organization size (in terms of number of switches) ' + 'this will exceeds the limits of the allowed API requests per second. You can try to ' + 'enable "Early Access" in the Meraki dashboard. In the Meraki special agent settings ' + f'switch from "{_SEC_NAME_SWITCH_PORTS_STATUSES}" to "{_SEC_NAME_ORG_SWITCH_PORTS_STATUSES}". ' + 'This will fetch all the switch data with one API request instead of one request for each ' + 'switch.' + ), + )), + # params from discovery + 'enabled': DictElement( + render_only=True, + parameter_form=String( + title=Title('Discovered admin state') + )), + 'status': DictElement( + render_only=True, + parameter_form=String( + title=Title('Discovered status') + ) + ), + 'speed': DictElement( + render_only=True, + parameter_form=String( + title=Title('Discovered speed') + ) + ) + }, + ) + + +rule_spec_cisco_meraki_switch_ports_statuses = CheckParameters( + name="cisco_meraki_switch_ports_statuses", + topic=Topic.NETWORKING, + parameter_form=_parameter_form, + title=Title("Cisco Meraki Switch Ports"), + condition=HostAndItemCondition(item_title=Title('Port ID')), +) + + +def _discovery_form(): + return Dictionary( + elements={ + 'discovered_port_states': DictElement( + parameter_form=MultipleChoice( + title=Title('Select Ports to discover'), + elements=[ + MultipleChoiceElement( + title=Title('Admin enabled'), + name='admin_enabled', + ), + MultipleChoiceElement( + title=Title('Admin disabled'), + name='admin_disabled', + ), + MultipleChoiceElement( + title=Title('Connected'), + name='connected', + ), + MultipleChoiceElement( + title=Title('Disconnected'), + name='disconnected', + ), + ], + help_text=Help('Select the port states for discovery'), + prefill=DefaultValue([ + 'admin_enabled', + 'admin_disabled', + 'connected', + 'disconnected', + ]) + )), + }, + ) + + +rule_spec_cisco_meraki_switch_ports_statuses_discovery = DiscoveryParameters( + name="discovery_cisco_meraki_switch_ports_statuses", + topic=Topic.GENERAL, + parameter_form=_discovery_form, + title=Title("Cisco Meraki Switch Ports"), +) diff --git a/source/cmk_addons_plugins/meraki/rulesets/wireless_device_ssid_status.py b/source/cmk_addons_plugins/meraki/rulesets/wireless_device_ssid_status.py new file mode 100644 index 0000000..8f0d170 --- /dev/null +++ b/source/cmk_addons_plugins/meraki/rulesets/wireless_device_ssid_status.py @@ -0,0 +1,43 @@ +#!/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 : 2024-02-02 +# File : cisco_meraki_org_wireless_status.py (WATO) + +# 2024-06-29: refactored for CMK 2.3 +# 2024-06-30 renamed from cisco_meraki_org_wireless_device_status.py in to wireless_device_ssid_status.py +# added SSI to the title + +from cmk.rulesets.v1 import Title +from cmk.rulesets.v1.form_specs import ( + DefaultValue, + DictElement, + Dictionary, + ServiceState, +) +from cmk.rulesets.v1.rule_specs import CheckParameters, HostAndItemCondition, Topic + + +def _parameter_form() -> Dictionary: + return Dictionary( + elements={ + 'state_if_not_enabled': DictElement( + parameter_form=ServiceState( + title=Title('Monitoring state if SSID is "not enabled"'), + prefill=DefaultValue(ServiceState.WARN), + )), + }, + ) + + +rule_spec_cisco_meraki_wireless_device_status = CheckParameters( + name="cisco_meraki_wireless_device_status", + topic=Topic.NETWORKING, + parameter_form=_parameter_form, + title=Title("Cisco Meraki Wireless device SSID"), + condition=HostAndItemCondition(item_title=Title("SSID")), +) diff --git a/source/cmk_plugins/cisco/rulesets/meraki.py b/source/cmk_plugins/cisco/rulesets/meraki.py new file mode 100644 index 0000000..95fc8b3 --- /dev/null +++ b/source/cmk_plugins/cisco/rulesets/meraki.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 +# Copyright (C) 2022 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. + +# +# file path must be exactly as the file it shadows :-( +# local/lip/python3/cmk/plugins/cisco/rulesets + + +from collections.abc import Iterable, Sequence + +from cmk.rulesets.v1 import Help, Label, Message, Title +from cmk.rulesets.v1.form_specs import ( + # BooleanChoice, + DefaultValue, + DictElement, + Dictionary, + FixedValue, + Integer, + List, + MultipleChoice, + MultipleChoiceElement, + Password, + Proxy, + String, + migrate_to_password, + migrate_to_proxy, +) + +from cmk.rulesets.v1.form_specs.validators import ValidationError, NumberInRange +from cmk.rulesets.v1.rule_specs import SpecialAgent, Topic +from cmk_addons.plugins.meraki.lib.utils import ( + _SEC_CACHE_APPLIANCE_PERFORMANCE, + _SEC_CACHE_APPLIANCE_UPLINKS_USAGE, + _SEC_CACHE_APPLIANCE_UPLINKS, + _SEC_CACHE_APPLIANCE_VPNS, + _SEC_CACHE_CELLULAR_UPLINKS, + _SEC_CACHE_DEVICE_INFO, + _SEC_CACHE_DEVICE_STATUSES, + _SEC_CACHE_DEVICE_UPLINKS_INFO, + _SEC_CACHE_LICENSES_OVERVIEW, + _SEC_CACHE_NETWORKS, + _SEC_CACHE_ORG_API_REQUESTS, + _SEC_CACHE_ORG_SWITCH_PORTS_STATUSES, + _SEC_CACHE_ORGANISATIONS, + _SEC_CACHE_SENSOR_READINGS, + _SEC_CACHE_SWITCH_PORTS_STATUSES, + _SEC_CACHE_WIRELESS_DEVICE_STATUS, + _SEC_CACHE_WIRELESS_ETHERNET_STATUSES, +) + +_SEC_NAME_APPLIANCE_UPLINKS = "appliance_uplinks" +_SEC_NAME_APPLIANCE_UPLINKS_USAGE = "appliance_uplinks_usage" +_SEC_NAME_APPLIANCE_VPNS = "appliance_vpns" +_SEC_NAME_APPLIANCE_PERFORMANCE = "appliance_performance" +_SEC_NAME_CELLULAR_UPLINKS = "cellular_uplinks" +_SEC_NAME_DEVICE_INFO = "device_info" +_SEC_NAME_DEVICE_STATUSES = "device_status" +_SEC_NAME_DEVICE_UPLINKS_INFO = "device_uplinks_info" +_SEC_NAME_LICENSES_OVERVIEW = "licenses_overview" +_SEC_NAME_NETWORKS = "networks" +_SEC_NAME_ORGANISATIONS = "organisations" +_SEC_NAME_ORG_API_REQUESTS = "api_requests_by_organization" +_SEC_NAME_SENSOR_READINGS = "sensor_readings" +_SEC_NAME_SWITCH_PORTS_STATUSES = "switch_ports_statuses" +_SEC_NAME_WIRELESS_DEVICE_STATUS = "wireless_device_status" +_SEC_NAME_WIRELESS_ETHERNET_STATUSES = "wireless_ethernet_statuses" +_SEC_NAME_ORG_SWITCH_PORTS_STATUSES = "org_switch_ports_statuses" + +_SEC_TITLE_DEVICE_INFO = 'Device info (Organization)' +_SEC_TITLE_NETWORKS = 'Network info (Organization)' +_SEC_TITLE_ORGANISATIONS = 'Organization (Agent)' +_SEC_TITLE_ORG_API_REQUESTS = 'API request (Organizaion)' +_SEC_TITLE_APPLIANCE_UPLINKS = 'Appliances uplinks (Organizaion)' +_SEC_TITLE_APPLIANCE_UPLINKS_USAGE = 'Appliances uplinks usage (Organizaion)' +_SEC_TITLE_APPLIANCE_VPNS = 'Appliances VPNs (Organizaion)' +_SEC_TITLE_APPLIANCE_PERFORMANCE = 'Appliances Utilization (Device)' +_SEC_TITLE_CELLULAR_UPLINKS = 'Cellular devices uplinks (Organizaion)' +_SEC_TITLE_DEVICE_STATUSES = 'Devices status (Organizaion)' +_SEC_TITLE_DEVICE_UPLINKS_INFO = 'Devices uplink info (Organizaion)' +_SEC_TITLE_LICENSES_OVERVIEW = 'Licenses overview (Organizaion)' +_SEC_TITLE_SENSOR_READINGS = 'Sensors readings (Organizaion)' +_SEC_TITLE_SWITCH_PORTS_STATUSES = 'Switch ports status (Device)' +_SEC_TITLE_WIRELESS_ETHERNET_STATUSES = 'Wireless devices ethernet status (Organizaion)' +_SEC_TITLE_WIRELESS_DEVICE_STATUS = 'Wireless devices SSIDs status (Device)' +_SEC_TITLE_ORG_SWITCH_PORTS_STATUSES = 'Switch port status (Organizaion/Early Access)' + + +class DuplicateInList: # pylint: disable=too-few-public-methods + """ Custom validator that ensures the validated list has no duplicate entries. """ + + def __init__( + self, + ) -> None: + pass + + @staticmethod + def _get_default_errmsg(_duplicates: Sequence) -> Message: + return Message(f"Duplicate element in list. Duplicate elements: {', '.join(_duplicates)}") + + def __call__(self, value: List[str] | None) -> None: + if not isinstance(value, list): + return + _duplicates = [value[i] for i, x in enumerate(value) if value.count(x) > 1] + _duplicates = list(set(_duplicates)) + if _duplicates: + raise ValidationError(message=self._get_default_errmsg(_duplicates)) + + +def _migrate_to_valid_ident(value: object) -> Sequence[str]: + if not isinstance(value, Iterable): + raise ValueError("Invalid value {value} for sections") + + name_mapping = { + "licenses-overview": "licenses_overview", + "device-statuses": "device_statuses", + "sensor-readings": "sensor_readings", + } + + # return [name_mapping.get(s, s) for s in value] + return [s.replace('-', '_') for s in value] + + +def _form_special_agent_cisco_meraki() -> Dictionary: + return Dictionary( + title=Title("Cisco Meraki"), + elements={ + "api_key": DictElement( + parameter_form=Password( + title=Title("API Key"), + migrate=migrate_to_password + ), + required=True, + ), + "proxy": DictElement( + parameter_form=Proxy( + migrate=migrate_to_proxy + ) + ), + "no_cache": DictElement( + parameter_form=FixedValue( # BooleanChoice needs 2 clicks :-( + title=Title("Disable Cache"), + help_text=Help( + "Never use cached information. By default the agent will cache received " + "data to avoid API limits and speed up the data retrievel." + ), + label=Label("API Cache is disabled"), + value=True, + ) + ), + 'org_id_as_prefix': DictElement( + parameter_form=FixedValue( + value=True, + title=Title('Uese organisation ID as host prefix'), + label=Label("The Organization-id will be used as host name prefix"), + help_text=Help( + 'The organisation ID will be used as prefix for the hostname (separated by a "\'"). Use ' + 'this option together with a "Hostname translation for piggybacked hosts" to add a ' + 'organisation prefix to the hosts from the Cisco Meraki cloud to avoid conflicting ' + 'hostnames. You can also use this option along with the "Dynamic host management" to ' + 'sort the host in organisation specific folders.' + ) + )), + + "excluded_sections": DictElement( + parameter_form=MultipleChoice( + title=Title("Exclude Sections"), + elements=[ + MultipleChoiceElement(name=_SEC_NAME_ORG_API_REQUESTS, + title=Title(_SEC_TITLE_ORG_API_REQUESTS)), + MultipleChoiceElement(name=_SEC_NAME_APPLIANCE_UPLINKS, + title=Title(_SEC_TITLE_APPLIANCE_UPLINKS)), + MultipleChoiceElement(name=_SEC_NAME_APPLIANCE_UPLINKS_USAGE, + title=Title(_SEC_TITLE_APPLIANCE_UPLINKS_USAGE)), + MultipleChoiceElement(name=_SEC_NAME_APPLIANCE_VPNS, title=Title(_SEC_TITLE_APPLIANCE_VPNS)), + MultipleChoiceElement(name=_SEC_NAME_APPLIANCE_PERFORMANCE, + title=Title(_SEC_TITLE_APPLIANCE_PERFORMANCE)), + MultipleChoiceElement(name=_SEC_NAME_CELLULAR_UPLINKS, + title=Title(_SEC_TITLE_CELLULAR_UPLINKS)), + MultipleChoiceElement(name=_SEC_NAME_DEVICE_STATUSES, title=Title(_SEC_TITLE_DEVICE_STATUSES)), + MultipleChoiceElement(name=_SEC_NAME_DEVICE_UPLINKS_INFO, + title=Title(_SEC_TITLE_DEVICE_UPLINKS_INFO)), + MultipleChoiceElement(name=_SEC_NAME_LICENSES_OVERVIEW, + title=Title(_SEC_TITLE_LICENSES_OVERVIEW)), + MultipleChoiceElement(name=_SEC_NAME_SENSOR_READINGS, title=Title(_SEC_TITLE_SENSOR_READINGS)), + MultipleChoiceElement(name=_SEC_NAME_SWITCH_PORTS_STATUSES, + title=Title(_SEC_TITLE_SWITCH_PORTS_STATUSES)), + MultipleChoiceElement(name=_SEC_NAME_WIRELESS_ETHERNET_STATUSES, + title=Title(_SEC_TITLE_WIRELESS_ETHERNET_STATUSES)), + MultipleChoiceElement(name=_SEC_NAME_WIRELESS_DEVICE_STATUS, + title=Title(_SEC_TITLE_WIRELESS_DEVICE_STATUS)), + MultipleChoiceElement(name=_SEC_NAME_ORG_SWITCH_PORTS_STATUSES, + title=Title(_SEC_TITLE_ORG_SWITCH_PORTS_STATUSES)), + ], + prefill=DefaultValue([ + _SEC_NAME_APPLIANCE_PERFORMANCE, + _SEC_NAME_SWITCH_PORTS_STATUSES, + _SEC_NAME_WIRELESS_DEVICE_STATUS, + _SEC_NAME_ORG_SWITCH_PORTS_STATUSES, + ]), + # migrate=_migrate_to_valid_ident, + ), + required=True, + ), + "orgs": DictElement( + parameter_form=List( + element_template=String(macro_support=True), title=Title("Organizations"), + custom_validate=(DuplicateInList(),), + ), + ), + "cache_per_section": DictElement( + parameter_form=Dictionary( + title=Title("Set Cache time per section"), + elements={ + sec_name: DictElement( + parameter_form=Integer( + title=Title(sec_title), + prefill=DefaultValue(sec_cache), + unit_symbol="minutes", + custom_validate=(NumberInRange(min_value=0),) + ) + ) for sec_name, sec_title, sec_cache in [ + (_SEC_NAME_APPLIANCE_PERFORMANCE, _SEC_TITLE_APPLIANCE_PERFORMANCE, _SEC_CACHE_APPLIANCE_PERFORMANCE), + (_SEC_NAME_APPLIANCE_UPLINKS_USAGE, _SEC_TITLE_APPLIANCE_UPLINKS_USAGE, _SEC_CACHE_APPLIANCE_UPLINKS_USAGE), + (_SEC_NAME_APPLIANCE_UPLINKS, _SEC_TITLE_APPLIANCE_UPLINKS, _SEC_CACHE_APPLIANCE_UPLINKS), + (_SEC_NAME_APPLIANCE_VPNS, _SEC_TITLE_APPLIANCE_VPNS, _SEC_CACHE_APPLIANCE_VPNS), + (_SEC_NAME_CELLULAR_UPLINKS, _SEC_TITLE_CELLULAR_UPLINKS, _SEC_CACHE_CELLULAR_UPLINKS), + (_SEC_NAME_DEVICE_INFO, _SEC_TITLE_DEVICE_INFO, _SEC_CACHE_DEVICE_INFO), + (_SEC_NAME_DEVICE_STATUSES, _SEC_TITLE_DEVICE_STATUSES, _SEC_CACHE_DEVICE_STATUSES), + (_SEC_NAME_DEVICE_UPLINKS_INFO, _SEC_TITLE_DEVICE_UPLINKS_INFO, _SEC_CACHE_DEVICE_UPLINKS_INFO), + (_SEC_NAME_LICENSES_OVERVIEW, _SEC_TITLE_LICENSES_OVERVIEW, _SEC_CACHE_LICENSES_OVERVIEW), + (_SEC_NAME_NETWORKS, _SEC_TITLE_NETWORKS, _SEC_CACHE_NETWORKS), + (_SEC_NAME_ORG_API_REQUESTS, _SEC_TITLE_ORG_API_REQUESTS, _SEC_CACHE_ORG_API_REQUESTS), + (_SEC_NAME_ORG_SWITCH_PORTS_STATUSES, _SEC_TITLE_ORG_SWITCH_PORTS_STATUSES, _SEC_CACHE_ORG_SWITCH_PORTS_STATUSES), + (_SEC_NAME_ORGANISATIONS, _SEC_TITLE_ORGANISATIONS, _SEC_CACHE_ORGANISATIONS), + (_SEC_NAME_SENSOR_READINGS, _SEC_TITLE_SENSOR_READINGS, _SEC_CACHE_SENSOR_READINGS), + (_SEC_NAME_SWITCH_PORTS_STATUSES, _SEC_TITLE_SWITCH_PORTS_STATUSES, _SEC_CACHE_SWITCH_PORTS_STATUSES), + (_SEC_NAME_WIRELESS_DEVICE_STATUS, _SEC_TITLE_WIRELESS_DEVICE_STATUS, _SEC_CACHE_WIRELESS_DEVICE_STATUS), + (_SEC_NAME_WIRELESS_ETHERNET_STATUSES, _SEC_TITLE_WIRELESS_ETHERNET_STATUSES, _SEC_CACHE_WIRELESS_ETHERNET_STATUSES), + ] + } + ) + ), + "sections": DictElement( + parameter_form=MultipleChoice( + title=Title("Sections"), + elements=[ + MultipleChoiceElement( + name="licenses_overview", title=Title("Organization licenses overview") + ), + MultipleChoiceElement( + name="device_statuses", title=Title("Organization device statuses") + ), + MultipleChoiceElement( + name="sensor_readings", title=Title("Organization sensor readings") + ), + ], + # migrate=_migrate_to_valid_ident, + ), + render_only=True, + ), + }, + ) + + +rule_spec_cisco_meraki = SpecialAgent( + name="cisco_meraki", + title=Title("Cisco Meraki"), + topic=Topic.APPLICATIONS, + parameter_form=_form_special_agent_cisco_meraki, +) diff --git a/source/agents/special/agent_cisco_meraki b/source/cmk_plugins/collection/libexec/agent_cisco_meraki similarity index 80% rename from source/agents/special/agent_cisco_meraki rename to source/cmk_plugins/collection/libexec/agent_cisco_meraki index d4ddb88..4f569d1 100755 --- a/source/agents/special/agent_cisco_meraki +++ b/source/cmk_plugins/collection/libexec/agent_cisco_meraki @@ -5,7 +5,7 @@ import sys -from cmk.special_agents.agent_cisco_meraki import main +from cmk_addons.plugins.meraki.lib import agent if __name__ == "__main__": - sys.exit(main()) + sys.exit(agent.main()) diff --git a/source/cmk_plugins/collection/server_side_calls/cisco_meraki.py b/source/cmk_plugins/collection/server_side_calls/cisco_meraki.py new file mode 100644 index 0000000..4f772f3 --- /dev/null +++ b/source/cmk_plugins/collection/server_side_calls/cisco_meraki.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +# Copyright (C) 2022 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 Iterator, Sequence, Mapping + +from pydantic import BaseModel + +from cmk.server_side_calls.v1 import ( + EnvProxy, + HostConfig, + NoProxy, + replace_macros, + Secret, + SpecialAgentCommand, + SpecialAgentConfig, + URLProxy, +) + +__param = { + 'api_key': Secret( + id=139915660185968, + format='%s', + pass_safely=True + ), + 'no_cache': True, + 'org_id_as_prefix': True, + 'excluded_sections': [ + 'appliance_performance', + 'switch_ports_statuses', + 'wireless_device_status', + 'org_switch_ports_statuses' + ], + 'orgs': ['1234', '670771'], + 'cache_per_section': { + 'appliance_performance': 0, + 'appliance_uplinks_usage': 0, + 'appliance_uplinks': 60, + 'appliance_vpns': 60, + 'cellular_uplinks': 60, + 'device_status': 60, + 'device_uplinks_info': 60, + 'licenses_overview': 600, + 'networks': 600, + 'api_requests_by_organization': 0, + 'org_switch_ports_statuses': 0, + 'organisations': 600, + 'sensor_readings': 0, + 'switch_ports_statuses': 0, + 'wireless_device_status': 30, + 'wireless_ethernet_statuses': 30 + } +} + + +class CachePerSection(BaseModel): + appliance_performance: int | None = None + appliance_uplinks_usage: int | None = None + appliance_uplinks: int | None = None + appliance_vpns: int | None = None + cellular_uplinks: int | None = None + device_info: int | None = None + device_status: int | None = None + device_uplinks_info: int | None = None + licenses_overview: int | None = None + networks: int | None = None + api_requests_by_organization: int | None = None + org_switch_ports_statuses: int | None = None + organisations: int | None = None + sensor_readings: int | None = None + switch_ports_statuses: int | None = None + wireless_device_status: int | None = None + wireless_ethernet_statuses: int | None = None + + +class Params(BaseModel): + api_key: Secret + proxy: URLProxy | NoProxy | EnvProxy | None = None + sections: Sequence[str] | None = None + orgs: Sequence[str] | None = None + excluded_sections: Sequence[str] | None = None + org_id_as_prefix: bool | None = None + no_cache: bool | None = None + cache_per_section: CachePerSection | None = None + + +def _agent_cisco_meraki_parser(params: Mapping[str, object]) -> Params: + # if 'api-requests-by-organization' in params.get('excluded_sections', []): + # print(params) + return Params.model_validate(params) + + +def agent_cisco_meraki_arguments( + params: Params, + host_config: HostConfig, +) -> Iterator[SpecialAgentCommand]: + # print(params) + + args: list[str | Secret] = [ + host_config.name, + params.api_key.unsafe(), + ] + + match params.proxy: + case URLProxy(url=url): + args += ["--proxy", url] + case EnvProxy(): + args += ["--proxy", "FROM_ENVIRONMENT"] + case NoProxy(): + args += ["--proxy", "NO_PROXY"] + + if params.sections is not None: + args.append("--sections") + args += [s.replace("_", "-") for s in params.sections] + + if params.excluded_sections is not None: + args.append("--excluded-sections") + args += [s.replace("_", "-") for s in params.excluded_sections] + + if params.orgs is not None: + args.append("--orgs") + args += [replace_macros(org, host_config.macros) for org in params.orgs] + + if params.cache_per_section is not None: + args.append("--cache-per-section") + args += [ + str(cache_value) if cache_value is not None else str(default_cache) for + cache_value, default_cache in [ + (params.cache_per_section.appliance_performance, 0), + (params.cache_per_section.appliance_uplinks_usage, 0), + (params.cache_per_section.appliance_uplinks, 60), + (params.cache_per_section.appliance_vpns, 60), + (params.cache_per_section.cellular_uplinks, 60), + (params.cache_per_section.device_info, 60), + (params.cache_per_section.device_status, 60), + (params.cache_per_section.device_uplinks_info, 60), + (params.cache_per_section.licenses_overview, 600), + (params.cache_per_section.networks, 600), + (params.cache_per_section.api_requests_by_organization, 0), + (params.cache_per_section.org_switch_ports_statuses, 0), + (params.cache_per_section.organisations, 600), + (params.cache_per_section.sensor_readings, 0), + (params.cache_per_section.switch_ports_statuses, 0), + (params.cache_per_section.wireless_device_status, 30), + (params.cache_per_section.wireless_ethernet_statuses, 30), + ] + ] + # default=[0, 0, 60, 60, 60, 60, 60, 60, 600, 600, 0, 0, 600, 0, 0, 30, 30] + # print(args) + + if params.org_id_as_prefix is True: + args.append("--org-id-as-prefix") + + if params.no_cache is True: + args.append("--no-cache") + + yield SpecialAgentCommand(command_arguments=args) + + +special_agent_cisco_meraki = SpecialAgentConfig( + name="cisco_meraki", + # parameter_parser=Params.model_validate, + parameter_parser=_agent_cisco_meraki_parser, + commands_function=agent_cisco_meraki_arguments, +) diff --git a/source/gui/metrics/cisco_meraki.py b/source/gui/metrics/cisco_meraki.py deleted file mode 100644 index 75e896c..0000000 --- a/source/gui/metrics/cisco_meraki.py +++ /dev/null @@ -1,332 +0,0 @@ -#!/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 : 2023-11-04 -# File : cisco_meraki.py (metrics) -# -# 2023-11-12: added wireless device status (channel, channel width, signal power) -# 2024-05-12: added switch port statuses and API return Codes -# 2024-06-24: fixed, wrong total dor SSID perfometer signal_power -> 30 - -from cmk.gui.i18n import _ - -from cmk.gui.plugins.metrics.utils import ( - check_metrics, - graph_info, - metric_info, - perfometer_info -) - -# -# license overview -# -metric_info['sum_licensed_devices'] = { - 'title': _('Licensed devices'), - 'unit': 'count', - 'color': '31/a', -} - -metric_info['remaining_time'] = { - 'title': _('Remaining time'), - 'unit': 's', - 'color': '26/a', -} - -graph_info['cisco_meraki.remaining_time'] = { - 'title': _('Cisco Meraki Licenses remaining time'), - 'metrics': [ - ('remaining_time', 'area'), - ], - 'scalars': [ - ('remaining_time:crit', _('crit')), - ('remaining_time:warn', _('warn')), - ], - 'range': (0, 'remaining_time:max'), -} - -graph_info['cisco_meraki.licensed_devices'] = { - 'title': _('Cisco Meraki Licensed devices'), - 'metrics': [ - ('sum_licensed_devices', 'area'), - ], - 'range': (0, 'sum_licensed_devices:max'), -} - -perfometer_info.append(('stacked', [ - { - 'type': 'logarithmic', - 'metric': 'remaining_time', - # 'half_value': 2592000.0, # ome month - 'half_value': 31104000.0, # ome year - 'exponent': 2, - }, - { - 'type': 'logarithmic', - 'metric': 'sum_licensed_devices', - 'half_value': 500.0, - 'exponent': 2, - } -])) - -# -# device_status -# -metric_info['last_reported'] = { - 'title': _('Last reported'), - 'unit': 's', - 'color': '26/a', -} - -graph_info['cisco_meraki.device_status'] = { - 'title': _('Cisco Meraki device status'), - 'metrics': [ - ('last_reported', 'area'), - ], - 'range': (0, 'last_reported:max'), - 'scalars': [ - ('last_reported:crit', _('crit')), - ('last_reported:warn', _('warn')), - ], -} - -perfometer_info.append({ - 'type': 'logarithmic', - 'metric': 'last_reported', - 'half_value': 7200, # 2 hours - 'exponent': 2, -}) - -# -# wireless devices status -# -metric_info["signal_power"] = { - "title": _("Power"), - "unit": "dbm", - "color": "#20c080", -} - -metric_info["channel_width"] = { - "title": _("Channel Width"), - "unit": "hz", - "color": "11/a", -} - -metric_info["channel"] = { - "title": _("Channel"), - "unit": "count", - "color": "21/a", -} - -graph_info['cisco_meraki.wireless_device_status.signal_power'] = { - 'title': _('Signal power'), - 'metrics': [ - ('signal_power', 'area'), - ], - 'range': (0, 'signal_power:max'), -} - -graph_info['cisco_meraki.wireless_device_status.channel_width'] = { - 'title': _('Channel Width'), - 'metrics': [ - ('channel_width', 'area'), - ], - 'range': (0, 'channel_width:max'), -} - -graph_info['cisco_meraki.wireless_device_status.channel'] = { - 'title': _('Channel'), - 'metrics': [ - ('channel', 'area'), - ], - 'range': (0, 'channel:max'), -} - -perfometer_info.append({ - 'type': 'linear', - 'segments': ['signal_power'], - 'total': 30, -}) - -# -# switch port statuses -# -# check_metrics['check_mk-cisco_meraki_organisations_api'] = { -# 'traffic_total': {'auto_graph': False}, -# 'traffic_sent': {'auto_graph': False}, -# 'traffic_received': {'auto_graph': False}, -# } -# -# metric_info["traffic_total"] = { -# "title": _("Total bandwidth"), -# "unit": "bits/s", -# "color": "11/a", -# } -# -# metric_info["traffic_sent"] = { -# "title": _("Output bandwidth"), -# "unit": "bits/s", -# "color": "#0080e0", -# } -# -# metric_info["traffic_received"] = { -# "title": _("Input bandwidth"), -# "unit": "bits/s", -# "color": "#00e060", -# } -# -# graph_info['cisco_meraki.switch_port_status.traffic'] = { -# 'title': _('Traffic'), -# 'metrics': [ -# ('traffic_received', 'area'), -# ('traffic_sent', '-area'), -# ], -# } -# perfometer_info.append( -# { -# "type": "dual", -# "perfometers": [ -# { -# "type": "logarithmic", -# "metric": "traffic_received", -# "half_value": 500000, -# "exponent": 5, -# }, -# { -# "type": "logarithmic", -# "metric": "traffic_sent", -# "half_value": 500000, -# "exponent": 5, -# }, -# ], -# } -# ) - -# -# API return Codes -# -check_metrics['check_mk-cisco_meraki_organisations_api'] = { - 'api_code_2xx': {'auto_graph': False}, - 'api_code_3xx': {'auto_graph': False}, - 'api_code_4xx': {'auto_graph': False}, - 'api_code_5xx': {'auto_graph': False}, -} - -metric_info["api_code_2xx"] = { - "title": _("Code 2xx"), - "unit": "count", - "color": "#00e060", -} -metric_info["api_code_3xx"] = { - "title": _("Code 3xx"), - "unit": "count", - "color": "#20e060", -} -metric_info["api_code_4xx"] = { - "title": _("Code 4xx"), - "unit": "count", - "color": "#0080e0", -} -metric_info["api_code_5xx"] = { - "title": _("Code 5xx"), - "unit": "count", - "color": "#2080e0", -} - - -graph_info['cisco_meraki.cisco_meraki_organisations_api.code'] = { - 'title': _('Cisco Meraki API response codes'), - 'metrics': [ - ('api_code_2xx', 'line'), - ('api_code_3xx', 'line'), - ('api_code_4xx', '-line'), - ('api_code_5xx', '-line'), - ], - 'optional_metrics': [ - 'api_code_2xx', - 'api_code_3xx', - 'api_code_4xx', - 'api_code_5xx', - ] -} -perfometer_info.append( - { - "type": "stacked", - "perfometers": [ - { - "type": "logarithmic", - "metric": "api_code_2xx", - "half_value": 100, - "exponent": 5, - }, - { - "type": "logarithmic", - "metric": "api_code_4xx", - "half_value": 100, - "exponent": 5, - }, - ], - } -) -perfometer_info.append({ - "type": "logarithmic", - "metric": "api_code_2xx", - "half_value": 100, - "exponent": 5, -}) -perfometer_info.append({ - "type": "logarithmic", - "metric": "api_code_4xx", - "half_value": 100, - "exponent": 5, -}) - -# appliance performance/utitlization -metric_info["utilization"] = { - "title": _("Utilization"), - "unit": "%", - "color": "16/a", -} -graph_info['cisco_meraki.cisco_meraki_appliance.utilization'] = { - 'title': _('Cisco Meraki Appliance Utilization'), - 'metrics': [ - ('utilization', 'area'), - ], - 'scalars': [ - ('utilization:crit', _('crit')), - ('utilization:warn', _('warn')), - ], - 'range': (0, 100), -} -perfometer_info.append({ - 'type': 'linear', - 'segments': ['utilization'], - 'total': 100, -}) - -# testing only -# metric_info["usage_out"] = { -# "title": _("Usage Out"), -# "unit": "count", -# "color": "#0080e0", -# } -# -# metric_info["usage_in"] = { -# "title": _("Usage In"), -# "unit": "count", -# "color": "#00e060", -# } -# -# graph_info['cisco_meraki.switch_port_status.usage'] = { -# 'title': _('Usage'), -# 'metrics': [ -# ('usage_in', 'area'), -# ('usage_out', '-area'), -# ], -# 'optional_metrics': [ -# 'usage_in', -# 'usage_out', -# ] -# } diff --git a/source/gui/wato/check_parameters/cisco_meraki_org_appliance_performance.py b/source/gui/wato/check_parameters/cisco_meraki_org_appliance_performance.py deleted file mode 100644 index 715f7f2..0000000 --- a/source/gui/wato/check_parameters/cisco_meraki_org_appliance_performance.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/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 : 2024-06-23 -# File : cisco_meraki_org_appliance_performance.py (WATO) - -from cmk.gui.i18n import _ -from cmk.gui.plugins.wato.utils import ( - CheckParameterRulespecWithoutItem, - rulespec_registry, - RulespecGroupCheckParametersNetworking, -) -from cmk.gui.valuespec import ( - Dictionary, - Integer, - Tuple, -) - - -def _parameter_valuespec_cisco_meraki_org_appliance_performance(): - return Dictionary( - title=_('Cisco Meraki Appliance Utilization'), - optional_keys=True, - elements=[ - ('levels_upper', - Tuple( - title=_('Upper Levels'), - elements=[ - Integer( - title=_("Warning at"), - unit='%', - default_value=60, - minvalue=0, - maxvalue=101, - ), - Integer( - title=_("Critical at"), - unit='%', - default_value=80, - minvalue=0, - maxvalue=101, - ), - ], - help=_( - 'The device utilization data reported to the Meraki' - ' dashboard is based on a load average measured over a' - ' period of one minute. The load value is returned in' - ' numeric values ranging from 1 through 100. A lower' - ' value indicates a lower load, and a higher value' - ' indicates a more intense workload. Currently, the' - ' device utilization value is calculated based upon the' - ' CPU utilization of the MX as well as its traffic load.' - ' If an MX device is consistently over 50% utilization' - ' during normal operation, upgrading to a higher' - ' throughput model or reducing the per-device load' - ' through horizontal scaling should be considered. For' - ' more information see:' - ' https://documentation.meraki.com-MX-Monitoring?and?' - 'Reporting-Device?Utiliyation'), - )), - ], - ) - - -rulespec_registry.register( - CheckParameterRulespecWithoutItem( - title=lambda: _('Cisco Meraki Appliance Utilization'), - check_group_name='cisco_meraki_org_appliance_performance', - group=RulespecGroupCheckParametersNetworking, - parameter_valuespec=_parameter_valuespec_cisco_meraki_org_appliance_performance, - match_type='dict', - ) -) diff --git a/source/gui/wato/check_parameters/cisco_meraki_org_appliance_uplinks.py b/source/gui/wato/check_parameters/cisco_meraki_org_appliance_uplinks.py deleted file mode 100644 index f0ef96c..0000000 --- a/source/gui/wato/check_parameters/cisco_meraki_org_appliance_uplinks.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/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 : 2023-11-05 -# File : cisco_meraki_org_appliance_uplinks.py (WATO) - -from cmk.gui.i18n import _ -from cmk.gui.plugins.wato.utils import ( - CheckParameterRulespecWithItem, - RulespecGroupCheckParametersNetworking, - rulespec_registry, -) -from cmk.gui.valuespec import ( - Dictionary, - FixedValue, - MonitoringState, - TextInput, -) - - -def _parameter_valuespec_cisco_meraki_org_appliance_uplinks(): - return Dictionary( - title=_('Cisco Meraki Appliance Uplinks'), - optional_keys=True, - elements=[ - ('status_map', - Dictionary( - title=_('Map uplink status to monitoring state'), - elements=[ - ("active", - MonitoringState( - title=_('Monitoring state for uplink state "active"'), - default_value=0, - )), - ("ready", - MonitoringState( - title=_('Monitoring state for uplink state "ready"'), - default_value=1, - )), - ("not connected", - MonitoringState( - title=_('Monitoring state for uplink state "not connected"'), - default_value=2, - )), - ("failed", - MonitoringState( - title=_('Monitoring state for uplink state "failed"'), - default_value=2, - )), - ] - )), - # not needed, if we don't want usage -> disable in agent - # ('show_traffic', - # FixedValue( - # True, - # title=_('Show bandwidth (use only with cache disabled)'), - # totext='Bandwidth monitoring enabled', - # help=_( - # 'Use only with cache disabled in the Meraki special agent settings. ' - # 'The throughput be based on the usage for the last 60 seconds.' - # ) - # )) - ], - ) - - -rulespec_registry.register( - CheckParameterRulespecWithItem( - title=lambda: _('Cisco Meraki Appliance uplinks'), - check_group_name='cisco_meraki_org_appliance_uplinks', - group=RulespecGroupCheckParametersNetworking, - parameter_valuespec=_parameter_valuespec_cisco_meraki_org_appliance_uplinks, - match_type='dict', - item_spec=lambda: TextInput( - title=_('The Uplink name'), - ), - ) -) diff --git a/source/gui/wato/check_parameters/cisco_meraki_org_appliance_vpns.py b/source/gui/wato/check_parameters/cisco_meraki_org_appliance_vpns.py deleted file mode 100644 index f3759c8..0000000 --- a/source/gui/wato/check_parameters/cisco_meraki_org_appliance_vpns.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/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 : 2023-11-05 -# File : cisco_meraki_org_appliance_vpns.py (WATO) - -from cmk.gui.i18n import _ -from cmk.gui.plugins.wato.utils import ( - CheckParameterRulespecWithItem, - rulespec_registry, - RulespecGroupCheckParametersNetworking, -) -from cmk.gui.valuespec import ( - Dictionary, - TextInput, - MonitoringState, -) - - -def _parameter_valuespec_cisco_meraki_org_appliance_vpns(): - return Dictionary( - title=_('Cisco Meraki Appliance VPNs'), - optional_keys=True, - elements=[ - ('status_not_reachable', - MonitoringState( - title=_('Monitoring state if the VPN peer is not "reachable"'), - default_value=1, - )), - ], - ) - - -rulespec_registry.register( - CheckParameterRulespecWithItem( - title=lambda: _('Cisco Meraki Appliance VPNs'), - check_group_name='cisco_meraki_org_appliance_vpns', - group=RulespecGroupCheckParametersNetworking, - parameter_valuespec=_parameter_valuespec_cisco_meraki_org_appliance_vpns, - match_type='dict', - item_spec=lambda: TextInput( - title=_('The peer name'), - ), - ) -) diff --git a/source/gui/wato/check_parameters/cisco_meraki_org_device_status.py b/source/gui/wato/check_parameters/cisco_meraki_org_device_status.py index b307596..f246265 100644 --- a/source/gui/wato/check_parameters/cisco_meraki_org_device_status.py +++ b/source/gui/wato/check_parameters/cisco_meraki_org_device_status.py @@ -8,9 +8,10 @@ # Date : 2023-11-04 # File : cisco_meraki_org_device_status.py (WATO) +# 2024-06-30: moved power supply part to cisco_meraki_org_device_status_ps.py (shadow built-in file) + from cmk.gui.i18n import _ from cmk.gui.plugins.wato.utils import ( - CheckParameterRulespecWithItem, CheckParameterRulespecWithoutItem, rulespec_registry, RulespecGroupCheckParametersHardware, @@ -18,7 +19,6 @@ from cmk.gui.plugins.wato.utils import ( from cmk.gui.valuespec import ( Integer, Dictionary, - TextInput, Tuple, MonitoringState, ) @@ -79,34 +79,3 @@ rulespec_registry.register( match_type="dict", ) ) - - -# -# Cisco Meraki Power Supply -# -def _parameter_valuespec_cisco_meraki_device_status_ps(): - return Dictionary( - title=_("Cisco Meraki Powersupply status"), - optional_keys=True, - elements=[ - ("state_not_powering", - MonitoringState( - title=_('Monitoring state if power supply is not "powering"'), - default_value=1, - )), - ], - ) - - -rulespec_registry.register( - CheckParameterRulespecWithItem( - title=lambda: _("Cisco Meraki Power supply"), - check_group_name="cisco_meraki_device_status_ps", - group=RulespecGroupCheckParametersHardware, - parameter_valuespec=_parameter_valuespec_cisco_meraki_device_status_ps, - match_type="dict", - item_spec=lambda: TextInput( - title=_("The Slot number"), - ), - ) -) diff --git a/source/gui/wato/check_parameters/cisco_meraki_org_device_status_ps.py b/source/gui/wato/check_parameters/cisco_meraki_org_device_status_ps.py new file mode 100644 index 0000000..335204a --- /dev/null +++ b/source/gui/wato/check_parameters/cisco_meraki_org_device_status_ps.py @@ -0,0 +1,54 @@ +#!/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 : 2024-06-30 +# File : cisco_meraki_org_device_status_ps.py (WATO) + +# 2024-06-30: created to shadow built-in file -> move rule from "Applications, Processes & Services" to "Hardware, BIOS" + +from cmk.gui.i18n import _ +from cmk.gui.plugins.wato.utils import ( + CheckParameterRulespecWithItem, + CheckParameterRulespecWithoutItem, + rulespec_registry, + RulespecGroupCheckParametersHardware, +) +from cmk.gui.valuespec import ( + Dictionary, + TextInput, + MonitoringState, +) + +# +# Cisco Meraki Power Supply -> now built-in in cmk 2.3 +# +def _parameter_valuespec_cisco_meraki_device_status_ps(): + return Dictionary( + title=_("Cisco Meraki Powersupply status"), + optional_keys=True, + elements=[ + ("state_not_powering", + MonitoringState( + title=_('Monitoring state if power supply is not "powering"'), + default_value=1, + )), + ], + ) + + +rulespec_registry.register( + CheckParameterRulespecWithItem( + title=lambda: _("Cisco Meraki Power supply"), + check_group_name="cisco_meraki_device_status_ps", + group=RulespecGroupCheckParametersHardware, + parameter_valuespec=_parameter_valuespec_cisco_meraki_device_status_ps, + match_type="dict", + item_spec=lambda: TextInput( + title=_("Slot number"), + ), + ) +) diff --git a/source/gui/wato/check_parameters/cisco_meraki_org_wireless_device_status.py b/source/gui/wato/check_parameters/cisco_meraki_org_wireless_device_status.py deleted file mode 100644 index 5a408e9..0000000 --- a/source/gui/wato/check_parameters/cisco_meraki_org_wireless_device_status.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/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 : 2024-02-02 -# File : cisco_meraki_org_wireless_status.py (WATO) - -from cmk.gui.i18n import _ -from cmk.gui.plugins.wato.utils import ( - CheckParameterRulespecWithItem, - rulespec_registry, - RulespecGroupCheckParametersNetworking, -) -from cmk.gui.valuespec import ( - Dictionary, - TextInput, - MonitoringState, -) - - -def _parameter_valuespec_cisco_meraki_wireless_status(): - return Dictionary( - title=_('Cisco Meraki Appliance Uplinks'), - optional_keys=True, - elements=[ - ('state_if_not_enabled', - MonitoringState( - title=_('Monitoring state if SSID is "not enabled"'), - default_value=1, - )), - ], - ) - - -rulespec_registry.register( - CheckParameterRulespecWithItem( - title=lambda: _('Cisco Meraki Wireless device'), - check_group_name='cisco_meraki_wireless_device_status', - group=RulespecGroupCheckParametersNetworking, - parameter_valuespec=_parameter_valuespec_cisco_meraki_wireless_status, - match_type='dict', - item_spec=lambda: TextInput(title=_('The SSID'), ), - ) -) diff --git a/source/gui/wato/check_parameters/cisco_meraki_organisations.py b/source/gui/wato/check_parameters/cisco_meraki_organisations.py deleted file mode 100644 index dcbd1a0..0000000 --- a/source/gui/wato/check_parameters/cisco_meraki_organisations.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/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 : 2023-11-11 -# File : cisco_meraki_organisations.py (wato plugin) - -# 2023-11-18: split from cisco_meraki_org_licenses_overview.py - - -from cmk.gui.i18n import _ -from cmk.gui.plugins.wato.utils import ( - DropdownChoice, - HostRulespec, - RulespecGroupCheckParametersDiscovery, - rulespec_registry, -) -from cmk.gui.valuespec import ( - Dictionary, -) - - -def _valuespec_discovery_meraki_organisations(): - return Dictionary( - title=_('Cisco Meraki Organisations (API/Licenses)'), - elements=[ - ('item_variant', - DropdownChoice( - title=_('Information to use as item'), - help=_( - 'You can select how to build the item for this service. By default the Organisation ID/name\n' - 'is used to stay compatible with the build in check. The information not used for the item\n' - 'will be added to the service output.' - ), - choices=[ - ('org_id', 'Organisation ID'), - ('org_name', 'Organisation name'), - ('org_id_name', 'Organisation ID/name'), - ], - default_value='org_id_name', - )), - ], - required_keys=['item_variant'], - ) - - -rulespec_registry.register( - HostRulespec( - group=RulespecGroupCheckParametersDiscovery, - match_type='dict', - name='discovery_meraki_organisations', - valuespec=_valuespec_discovery_meraki_organisations, - )) diff --git a/source/gui/wato/check_parameters/cisco_meraki_organisations_api.py b/source/gui/wato/check_parameters/cisco_meraki_organisations_api.py deleted file mode 100644 index bc47b9c..0000000 --- a/source/gui/wato/check_parameters/cisco_meraki_organisations_api.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/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 : 2023-11-18 -# File : cisco_meraki_organisations_api.py (wato plugin) - -from cmk.gui.i18n import _ -from cmk.gui.plugins.wato.utils import ( - CheckParameterRulespecWithItem, - rulespec_registry, - RulespecGroupCheckParametersApplications, -) -from cmk.gui.valuespec import ( - Dictionary, - TextInput, - MonitoringState, -) - - -def _parameter_valuespec_cisco_meraki_organisations_api(): - return Dictionary( - title=_("Cisco Meraki Organisation API"), - optional_keys=True, - elements=[ - ("state_api_not_enabled", - MonitoringState( - title=_('Monitoring state if API is not enabled'), - default_value=1, - )), - ], - ignored_keys=[ - 'internal_item_name', - 'old_item_name', - 'item_variant', - ], - ) - - -rulespec_registry.register( - CheckParameterRulespecWithItem( - title=lambda: _("Cisco Meraki Organisation API"), - check_group_name="cisco_meraki_organisations_api", - group=RulespecGroupCheckParametersApplications, - parameter_valuespec=_parameter_valuespec_cisco_meraki_organisations_api, - item_spec=lambda: TextInput(title=_("The organisation"), ), - match_type="dict", - ) -) diff --git a/source/gui/wato/check_parameters/cisco_meraki_switch_ports_statuses.py b/source/gui/wato/check_parameters/cisco_meraki_switch_ports_statuses.py deleted file mode 100644 index b312f69..0000000 --- a/source/gui/wato/check_parameters/cisco_meraki_switch_ports_statuses.py +++ /dev/null @@ -1,132 +0,0 @@ -#!/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 : 2024-02-02 -# File : cisco_meraki_switch_ports_statuses.py (WATO) - -# 2024-05-12: added support for MerakiGetOrganizationSwitchPortsStatusesBySwitch (Early Access) -# added traffic counters as perfdata -# 2024-05-19: reworked switch port traffic -# 2024-05-20: added discovery rule for port status - -from cmk.gui.i18n import _ -from cmk.gui.plugins.wato.utils import ( - CheckParameterRulespecWithItem, - HostRulespec, - RulespecGroupCheckParametersDiscovery, - RulespecGroupCheckParametersNetworking, - rulespec_registry, -) -from cmk.gui.valuespec import ( - Dictionary, - ListChoice, - FixedValue, - MonitoringState, - TextInput, -) - -from cmk.base.plugins.agent_based.utils.cisco_meraki import ( - _SEC_NAME_SWITCH_PORTS_STATUSES, # type: ignore[import] - # Early Access - _SEC_NAME_ORG_SWITCH_PORTS_STATUSES, # type: ignore[import] -) - - -def _parameter_valuespec_cisco_meraki_switch_ports_statuses(): - return Dictionary( - title=_('Cisco Meraki Appliance Uplinks'), - optional_keys=True, - elements=[ - ('state_disabled', - MonitoringState( - title=_('Monitoring state if port is "disabled"'), - default_value=0, - )), - ('state_not_connected', - MonitoringState( - title=_('Monitoring state if port is "not connected"'), - default_value=0, - )), - ('state_not_full_duplex', - MonitoringState( - title=_('Monitoring state if port is "not full duplex"'), - default_value=1, - )), - ('state_speed_change', - MonitoringState( - title=_('Monitoring state if speed is changed'), - default_value=1, - )), - ('state_admin_change', - MonitoringState( - title=_('Monitoring state if admin state is changed'), - default_value=1, - )), - ('state_op_change', - MonitoringState( - title=_('Monitoring state if operational state is changed'), - default_value=1, - )), - ('show_traffic', - FixedValue( - True, - title=_('Show bandwidth (use only with cache disabled)'), - totext='Bandwidth monitoring enabled', - help=_( - 'Use only with cache disabled in the Meraki special agent settings. ' - 'Depending on your Meraki organization size (in terms of number of switches) ' - 'this will exceeds the limits of the allowed API requests per second. You can try to ' - 'enable "Early Access" in the Meraki dashboard. In the Meraki special agent settings ' - f'switch from "{_SEC_NAME_SWITCH_PORTS_STATUSES}" to "{_SEC_NAME_ORG_SWITCH_PORTS_STATUSES}". ' - 'This will fetch all the switch data with one API request instead of one request for each switch.' - ), - )), - ], - ) - - -rulespec_registry.register( - CheckParameterRulespecWithItem( - title=lambda: _('Cisco Meraki Switch Ports'), - check_group_name='cisco_meraki_switch_ports_statuses', - group=RulespecGroupCheckParametersNetworking, - parameter_valuespec=_parameter_valuespec_cisco_meraki_switch_ports_statuses, - match_type='dict', - item_spec=lambda: TextInput(title=_('The Port ID'), ), - ) -) - - - -def _valuespec_discovery_cisco_meraki_switch_ports_statuses(): - return Dictionary( - title=_('Cisco Meraki Switch Ports'), - elements=[ - ('discovered_port_states', - ListChoice( - title=_('Select Ports to discover'), - choices=[ - (True, _('Admin enabled')), - (False, _('Admin disabled')), - ('Connected', _('Connected')), - ('Disconnected', _('Disconnected')), - ], - help=_('Select the port states for discovery'), - default_value=[True, False, 'Connected', 'Disconnected'], - )), - ], - required_keys=['item_variant'], - ) - - -rulespec_registry.register( - HostRulespec( - group=RulespecGroupCheckParametersDiscovery, - match_type='dict', - name='discovery_cisco_meraki_switch_ports_statuses', - valuespec=_valuespec_discovery_cisco_meraki_switch_ports_statuses, - )) \ No newline at end of file diff --git a/source/packages/cisco_meraki b/source/packages/cisco_meraki index 6f2a23d..901bfc2 100644 --- a/source/packages/cisco_meraki +++ b/source/packages/cisco_meraki @@ -22,45 +22,48 @@ '\n' 'For the Appliance Uplinks Usage and Wireless Devices Ethernet ' 'Statuses \n' - 'checks you need to update the Merkai SDK to version 1.39.0 at ' + 'checks you need to update the Meraki SDK to version 1.39.0 at ' 'least.\n' 'https://thl-cmk.hopto.org/gitlab/checkmk/cisco/meraki/cisco_meraki/-/raw/master/mkp/MerkaiSDK-1.39.0-202311-10.mkp\n' '\n' 'The latest SDK can be found here: ' 'https://github.com/meraki/dashboard-api-python\n', 'download_url': 'https://thl-cmk.hopto.org', - 'files': {'agent_based': ['utils/cisco_meraki.py', - 'cisco_meraki_org_appliance_uplinks.py', - 'cisco_meraki_org_appliance_vpns.py', - 'cisco_meraki_org_device_info.py', + 'files': {'agent_based': ['cisco_meraki_org_device_info.py', 'cisco_meraki_org_device_status.py', - 'cisco_meraki_org_device_uplinks.py', 'cisco_meraki_org_licenses_overview.py', - 'cisco_meraki_switch_ports_statuses.py', - 'cisco_meraki_org_wireless_device_status.py', - 'cisco_meraki_org_wireless_ethernet_statuses.py', - 'cisco_meraki_org_cellular_uplinks.py', - 'cisco_meraki_organisations_api.py', - 'cisco_meraki_org_networks.py', - 'cisco_meraki_org_appliance_performance.py'], - 'agents': ['special/agent_cisco_meraki'], - 'checks': ['agent_cisco_meraki'], - 'gui': ['metrics/cisco_meraki.py', - 'wato/check_parameters/cisco_meraki_org_appliance_uplinks.py', - 'wato/check_parameters/cisco_meraki_org_appliance_vpns.py', - 'wato/check_parameters/cisco_meraki_org_device_status.py', - 'wato/check_parameters/cisco_meraki_org_licenses_overviewi.py', - 'wato/check_parameters/cisco_meraki_organisations.py', - 'wato/check_parameters/cisco_meraki_organisations_api.py', - 'wato/check_parameters/cisco_meraki_org_wireless_device_status.py', - 'wato/check_parameters/cisco_meraki_switch_ports_statuses.py', - 'wato/check_parameters/cisco_meraki_org_appliance_performance.py'], - 'lib': ['python3/cmk/special_agents/agent_cisco_meraki.py'], - 'web': ['plugins/views/cisco_meraki.py', - 'plugins/wato/agent_cisco_meraki.py']}, + 'cisco_meraki_org_sensor_readings.py_'], + 'cmk_addons_plugins': ['meraki/agent_based/appliance_performance.py', + 'meraki/agent_based/appliance_uplinks.py', + 'meraki/agent_based/appliance_vpns.py', + 'meraki/agent_based/cellular_uplinks.py', + 'meraki/agent_based/device_uplinks.py', + 'meraki/agent_based/networks.py', + 'meraki/agent_based/organisations_api.py', + 'meraki/agent_based/switch_ports_statuses.py', + 'meraki/agent_based/wireless_device_ssid_status.py', + 'meraki/agent_based/wireless_ethernet_statuses.py', + 'meraki/graphing/packages.py', + 'meraki/lib/agent.py', + 'meraki/lib/utils.py', + 'meraki/rulesets/appliance_performance.py', + 'meraki/rulesets/appliance_uplinks.py', + 'meraki/rulesets/appliance_vpns.py', + 'meraki/rulesets/licenses_overviewi.py', + 'meraki/rulesets/organisations.py', + 'meraki/rulesets/organisations_api.py', + 'meraki/rulesets/switch_ports_statuses.py', + 'meraki/rulesets/wireless_device_ssid_status.py'], + 'cmk_plugins': ['cisco/rulesets/meraki.py', + 'collection/libexec/agent_cisco_meraki', + 'collection/server_side_calls/cisco_meraki.py'], + 'gui': ['wato/check_parameters/cisco_meraki_org_device_status.py', + 'wato/check_parameters/cisco_meraki_org_device_status_ps.py', + 'wato/check_parameters/cisco_meraki_org_licenses_overviewi.py'], + 'web': ['plugins/views/cisco_meraki.py']}, 'name': 'cisco_meraki', 'title': 'Cisco Meraki special agent', - 'version': '1.3.2-20240626', - 'version.min_required': '2.2.0b1', - 'version.packaged': '2.2.0p27', - 'version.usable_until': '2.3.0b1'} + 'version': '1.3.2-20240660', + 'version.min_required': '2.3.0b1', + 'version.packaged': 'cmk-mkp-tool 0.2.0', + 'version.usable_until': '2.4.0b1'} diff --git a/source/web/plugins/views/cisco_meraki.py b/source/web/plugins/views/cisco_meraki.py index 89d1512..9ec7646 100644 --- a/source/web/plugins/views/cisco_meraki.py +++ b/source/web/plugins/views/cisco_meraki.py @@ -11,9 +11,8 @@ # 2023-11-17: moved file from local/lib/ structure to local/share/ structure to avoid errors in web.log # 2023-11-19: added MT device -from cmk.gui.views.inventory.registry import inventory_displayhints - from cmk.gui.i18n import _l +from cmk.gui.views.inventory.registry import inventory_displayhints inventory_displayhints.update({ '.networking.uplinks:': { @@ -118,9 +117,3 @@ inventory_displayhints.update({ '.software.applications.cisco_meraki.networks:*.tags': {'title': _l('Tags')}, '.software.applications.cisco_meraki.networks:*.is_bound_to_template': {'title': _l('Is bound to template')}, }) -# cleanup build in display hints -# inventory_displayhints.pop('.software.configuration.organisation') -# inventory_displayhints.pop('.software.configuration.organisation.organisation_id') -# inventory_displayhints.pop('.software.configuration.organisation.organisation_name') -# inventory_displayhints.pop('.software.configuration.organisation.network_id') -# inventory_displayhints.pop('.software.configuration.organisation.address') diff --git a/source/web/plugins/wato/agent_cisco_meraki.py b/source/web/plugins/wato/agent_cisco_meraki.py deleted file mode 100644 index 60192b5..0000000 --- a/source/web/plugins/wato/agent_cisco_meraki.py +++ /dev/null @@ -1,220 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (C) 2022 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. - -# -# NOTE: to use the updated cisco meraki special agent WATO via ~/local structure you need to remove/rename the -# original file (~/lib/check_mk/gui/plugins/wato/special_agents/cisco_meraki.py). At the moment you -# can not supersede a special agent wato file via ~/local structure :-( -# - -# enhancements by thl-cmk[at]outlook[dot]com, https://thl-cmk.hopto.org -# - added check for duplicate organisation IDs -# - added some (basic) online help -# - changed clarified title of "orgs" from "Organisations" to "Organisation IDs" -# - added section titles from utils/cisco_meraki.py (reuse) -# - added option for host prefix/suffix/case per organisation -> needed for dynamic host management -# - added section names from utils/cisco_meraki.py (reuse with special agent) -# 2023-11-18: moved from ~/local/lib/check_mk/gui/plugins/wato to ~/local/share/check_mk/web/plugins/wato -# changed sections to excluded_sections -# 2023-11-22: replaced host_suffix_prefix option by org_id_as_prefix -# changed excluded_sections option from DualListChoice to ListChoice to avoid the "Selected" header -# in conjunction with "excluded Sections" title -# 2024-05-15: added api_key to required_keys -# 2024-06-23: added cache time per section -> not nice but should work. - -from typing import List - -from cmk.gui.i18n import _ -from cmk.gui.plugins.wato.special_agents.common import ( - RulespecGroupDatasourceProgramsApps -) -from cmk.gui.plugins.wato.utils import ( - HostRulespec, - HTTPProxyReference, - IndividualOrStoredPassword, - MKUserError, - rulespec_registry, -) -from cmk.gui.valuespec import ( - Dictionary, - FixedValue, - Integer, - ListChoice, - ListOfStrings, - Tuple, - ValueSpec, -) -from cmk.base.plugins.agent_based.utils.cisco_meraki import ( - # _SEC_NAME_DEVICE_INFO, - # _SEC_NAME_NETWORKS, - # _SEC_NAME_ORGANISATIONS, - _SEC_NAME_ORG_API_REQUESTS, # type: ignore[import] - _SEC_NAME_LICENSES_OVERVIEW, # type: ignore[import] - _SEC_NAME_DEVICE_STATUSES, # type: ignore[import] - _SEC_NAME_SENSOR_READINGS, # type: ignore[import] - _SEC_NAME_DEVICE_UPLINKS_INFO, # type: ignore[import] - _SEC_NAME_APPLIANCE_UPLINKS, # type: ignore[import] - _SEC_NAME_APPLIANCE_UPLINKS_USAGE, # type: ignore[import] - _SEC_NAME_APPLIANCE_VPNS, # type: ignore[import] - _SEC_NAME_APPLIANCE_PERFORMANCE, - _SEC_NAME_SWITCH_PORTS_STATUSES, # type: ignore[import] - _SEC_NAME_WIRELESS_ETHERNET_STATUSES, # type: ignore[import] - _SEC_NAME_WIRELESS_DEVICE_STATUS, # type: ignore[import] - _SEC_NAME_CELLULAR_UPLINKS, # type: ignore[import] - # Early Access - _SEC_NAME_ORG_SWITCH_PORTS_STATUSES, # type: ignore[import] -) - -_SEC_TITLE_DEVICE_INFO = _('Device info (Organization)') -_SEC_TITLE_NETWORKS = _('Network info (Organization)') -_SEC_TITLE_ORGANISATIONS = _('Organization (Agent)') -_SEC_TITLE_ORG_API_REQUESTS = _('API request (Organizaion)') -_SEC_TITLE_APPLIANCE_UPLINKS = _('Appliances uplinks (Organizaion)') -_SEC_TITLE_APPLIANCE_UPLINKS_USAGE = _( - 'Appliances uplinks usage (Organizaion)') -_SEC_TITLE_APPLIANCE_VPNS = _('Appliances VPNs (Organizaion)') -_SEC_TITLE_APPLIANCE_PERFORMANCE = _('Appliances Utilization (Device)') -_SEC_TITLE_CELLULAR_UPLINKS = _('Cellular devices uplinks (Organizaion)') -_SEC_TITLE_DEVICE_STATUSES = _('Devices status (Organizaion)') -_SEC_TITLE_DEVICE_UPLINKS_INFO = _('Devices uplink info (Organizaion)') -_SEC_TITLE_LICENSES_OVERVIEW = _('Licenses overview (Organizaion)') -_SEC_TITLE_SENSOR_READINGS = _('Sensors readings (Organizaion)') -_SEC_TITLE_SWITCH_PORTS_STATUSES = _('Switch ports status (Device)') -_SEC_TITLE_WIRELESS_ETHERNET_STATUSES = _( - 'Wireless devices ethernet status (Organizaion)') -_SEC_TITLE_WIRELESS_DEVICE_STATUS = _('Wireless devices SSIDs status (Device)') -_SEC_TITLE_ORG_SWITCH_PORTS_STATUSES = _( - 'Switch port status (Organizaion/Early Access)') - - -def _validate_orgs(value: List[str] | None, var_prefix: str): - # Check for duplicate Organisations - if value is None: - return - _p = list(set(value.copy())) - if len(_p) != len(value): - raise MKUserError(var_prefix, _('Duplicate Organisation found')) - - for org_id in value: - if not org_id.isdigit(): - raise MKUserError( - var_prefix, _( - f'Not a valid Organisation ID {org_id}. Organisation IDs' - ' are all digits' - ) - ) - - -def _valuespec_special_agent_cisco_meraki() -> ValueSpec: - return Dictionary( - title=_('Cisco Meraki'), - elements=[ - ('api_key', IndividualOrStoredPassword( - title=_('API Key'), - allow_empty=False, - help=_('The key to access the Cisco Meraki Cloud Rest API.') - )), - ('proxy', HTTPProxyReference(),), - ('no_cache', FixedValue( - value=True, - title=_('Disable Cache'), - totext=_(''), - help=_( - 'Never use cached information. By default the agent will cache received ' - 'data to avoid API limits and speed up the data retrievel.' - ) - )), - ('org_id_as_prefix', FixedValue( - value=True, - title=_('Uese organisation ID as host prefix'), - totext=_(''), - help=_( - 'The organisation ID will be used as prefix for the hostname (separated by a "\'"). Use ' - 'this option together with a "Hostname translation for piggybacked hosts" to add a organisation ' - 'prefix to the hosts from the Cisco Meraki cloud to avoid conflicting hostnames. You can also use ' - 'this option along with the "Dynamic host management" to sort the host in organisation specific ' - 'folders.' - ) - )), - ('excluded_sections', - ListChoice( - title=_('excluded Sections'), - choices=[ - (_SEC_NAME_ORG_API_REQUESTS, _SEC_TITLE_ORG_API_REQUESTS), - (_SEC_NAME_APPLIANCE_UPLINKS, _SEC_TITLE_APPLIANCE_UPLINKS), - (_SEC_NAME_APPLIANCE_UPLINKS_USAGE, - _SEC_TITLE_APPLIANCE_UPLINKS_USAGE), - (_SEC_NAME_APPLIANCE_VPNS, _SEC_TITLE_APPLIANCE_VPNS), - (_SEC_NAME_APPLIANCE_PERFORMANCE, - _SEC_TITLE_APPLIANCE_PERFORMANCE), - (_SEC_NAME_CELLULAR_UPLINKS, _SEC_TITLE_CELLULAR_UPLINKS), - (_SEC_NAME_DEVICE_STATUSES, _SEC_TITLE_DEVICE_STATUSES), - (_SEC_NAME_DEVICE_UPLINKS_INFO, - _SEC_TITLE_DEVICE_UPLINKS_INFO), - (_SEC_NAME_LICENSES_OVERVIEW, _SEC_TITLE_LICENSES_OVERVIEW), - (_SEC_NAME_SENSOR_READINGS, _SEC_TITLE_SENSOR_READINGS), - (_SEC_NAME_SWITCH_PORTS_STATUSES, - _SEC_TITLE_SWITCH_PORTS_STATUSES), - (_SEC_NAME_WIRELESS_ETHERNET_STATUSES, - _SEC_TITLE_WIRELESS_ETHERNET_STATUSES), - (_SEC_NAME_WIRELESS_DEVICE_STATUS, - _SEC_TITLE_WIRELESS_DEVICE_STATUS), - (_SEC_NAME_ORG_SWITCH_PORTS_STATUSES, - _SEC_TITLE_ORG_SWITCH_PORTS_STATUSES), - ], - help=_( - 'Query only the selected sections. Default is Query all sections.'), - default_value=[ - _SEC_NAME_ORG_SWITCH_PORTS_STATUSES, - _SEC_NAME_APPLIANCE_PERFORMANCE, - _SEC_NAME_SWITCH_PORTS_STATUSES, - _SEC_NAME_WIRELESS_DEVICE_STATUS, - ], - )), - ('orgs', - ListOfStrings( - title=_('Organisation IDs'), - help=_( - 'List of Organisation IDs to query. Defaulr is all Organisation IDs'), - allow_empty=False, - validate=_validate_orgs, - )), - ('cache_per_section', - Tuple( - title='Set Cache time per section', - elements=[ - Integer(title=_SEC_TITLE_APPLIANCE_PERFORMANCE, minvalue=0, unit='minutes', default_value=0), - Integer(title=_SEC_TITLE_APPLIANCE_UPLINKS_USAGE, minvalue=0, unit='minutes', default_value=0), - Integer(title=_SEC_TITLE_APPLIANCE_UPLINKS, minvalue=0, unit='minutes', default_value=60), - Integer(title=_SEC_TITLE_APPLIANCE_VPNS, minvalue=0, unit='minutes', default_value=60), - Integer(title=_SEC_TITLE_CELLULAR_UPLINKS, minvalue=0, unit='minutes', default_value=60), - Integer(title=_SEC_TITLE_DEVICE_INFO, minvalue=0, unit='minutes', default_value=60), - Integer(title=_SEC_TITLE_DEVICE_STATUSES, minvalue=0, unit='minutes', default_value=60), - Integer(title=_SEC_TITLE_DEVICE_UPLINKS_INFO, minvalue=0, unit='minutes', default_value=60), - Integer(title=_SEC_TITLE_LICENSES_OVERVIEW, minvalue=0, unit='minutes', default_value=600), - Integer(title=_SEC_TITLE_NETWORKS, minvalue=0, unit='minutes', default_value=600), - Integer(title=_SEC_TITLE_ORG_API_REQUESTS, minvalue=0, unit='minutes', default_value=0), - Integer(title=_SEC_TITLE_ORG_SWITCH_PORTS_STATUSES, minvalue=0, unit='minutes', default_value=0), - Integer(title=_SEC_TITLE_ORGANISATIONS, minvalue=0, unit='minutes', default_value=600), - Integer(title=_SEC_TITLE_SENSOR_READINGS, minvalue=0, unit='minutes', default_value=0), - Integer(title=_SEC_TITLE_SWITCH_PORTS_STATUSES, minvalue=0, unit='minutes', default_value=0), - Integer(title=_SEC_TITLE_WIRELESS_DEVICE_STATUS, minvalue=0, unit='minutes', default_value=30), - Integer(title=_SEC_TITLE_WIRELESS_ETHERNET_STATUSES, minvalue=0, unit='minutes', default_value=30), - ], - )) - ], - optional_keys=True, - ignored_keys=['sections', 'host_suffix_prefix'], - required_keys=['excluded_sections', 'api_key'], - ) - - -rulespec_registry.register( - HostRulespec( - group=RulespecGroupDatasourceProgramsApps, - name='special_agents:cisco_meraki', - valuespec=_valuespec_special_agent_cisco_meraki, - ) -) -- GitLab