From 14d5e8d05c7171687a4e1457f4bbeeae0d9919a5 Mon Sep 17 00:00:00 2001 From: "th.l" <thl-cmk@outlook.com> Date: Tue, 17 Dec 2024 11:44:43 +0100 Subject: [PATCH] added support for Network Visualization - wireless_ethernet_statuses.py - incompatible: changed service from Port to Interface - added interface inventory - switch_ports_statuses.py - incompatible: changed service from Port to Interface - incompatible: reworked service discovery rule (Cisco Meraki Switch Ports) - added interface inventory - added host label nvdct/has_lldp_neighbours --- README.md | 2 +- mkp/cisco_meraki-1.4.1-20241217.mkp | Bin 0 -> 44015 bytes .../cisco_meraki_org_device_status.py | 2 +- .../meraki/agent_based/appliance_uplinks.py | 22 +- .../meraki/agent_based/appliance_vpns.py | 4 +- .../meraki/agent_based/cellular_uplinks.py | 54 +-- .../agent_based/switch_ports_statuses.py | 370 ++++++++++++------ .../wireless_device_ssid_status.py | 4 +- .../agent_based/wireless_ethernet_statuses.py | 34 +- source/cmk_addons_plugins/meraki/lib/agent.py | 297 +++++++++----- source/cmk_addons_plugins/meraki/lib/utils.py | 56 +-- .../meraki/rulesets/switch_ports_statuses.py | 71 ++-- source/packages/cisco_meraki | 2 +- 13 files changed, 587 insertions(+), 331 deletions(-) create mode 100644 mkp/cisco_meraki-1.4.1-20241217.mkp diff --git a/README.md b/README.md index 0cc147b..20dce74 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[PACKAGE]: ../../raw/master/mkp/cisco_meraki-1.3.7-20241116.mkp "cisco_meraki-1.3.7-20241116.mkp" +[PACKAGE]: ../../raw/master/mkp/cisco_meraki-1.4.1-20241217.mkp "cisco_meraki-1.4.1-20241217.mkp" [SDK]: ../../raw/master/mkp/MerakiSDK-1.52.0-20241116.mkp "MerakiSDK-1.52.0-20241116.mkp" # Cisco Meraki special agent diff --git a/mkp/cisco_meraki-1.4.1-20241217.mkp b/mkp/cisco_meraki-1.4.1-20241217.mkp new file mode 100644 index 0000000000000000000000000000000000000000..517e79d7a5a922b5e93427c2db53138760c470c4 GIT binary patch literal 44015 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	que0MD?CVwsU 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 b{6E9n|M7zO4=UELdI!!y<F$iTLxKH2=37)W literal 0 HcmV?d00001 diff --git a/source/agent_based/cisco_meraki_org_device_status.py b/source/agent_based/cisco_meraki_org_device_status.py index 56b1c6f..5a0d4ac 100644 --- a/source/agent_based/cisco_meraki_org_device_status.py +++ b/source/agent_based/cisco_meraki_org_device_status.py @@ -3,7 +3,7 @@ # 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 cmk.gui.forms import section # enhancements by thl-cmk[at]outlook[dot]com, https://thl-cmk.hopto.org # - made device status configurable via WATO # - added last_reported as check_levels, levels_upper can be configured via WATO diff --git a/source/cmk_addons_plugins/meraki/agent_based/appliance_uplinks.py b/source/cmk_addons_plugins/meraki/agent_based/appliance_uplinks.py index 7a093bf..4fb45ed 100644 --- a/source/cmk_addons_plugins/meraki/agent_based/appliance_uplinks.py +++ b/source/cmk_addons_plugins/meraki/agent_based/appliance_uplinks.py @@ -14,11 +14,12 @@ # 2024-05-15: fixed typo in output of uplink.received (in -> In) (ThX to Rickard Eriksson) # 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-04-24: fixed, we can have no traffic if uplink is not connected # 2024-06-29: refactored for CMK 2.3 -# changed service name from "Appliance Uplink" to "Uplink" +# 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 +# 2024-12-13: added connecting to _STATUS_MAP from collections.abc import Mapping from dataclasses import dataclass @@ -61,8 +62,8 @@ __appliance_uplinks = [ 'publicIp': '20.197.135.251', 'secondaryDns': '9.9.9.9', 'status': 'active', - "received": 52320006, # bytes - "sent": 52928038, # bytes + 'received': 52320006, # bytes + 'sent': 52928038, # bytes }, { 'gateway': '192.168.5.100', @@ -78,7 +79,7 @@ __appliance_uplinks = [ } ] -_LAST_REPORTED_AT = "%Y-%m-%dT%H:%M:%SZ" +_LAST_REPORTED_AT = '%Y-%m-%dT%H:%M:%SZ' @dataclass(frozen=True) @@ -152,7 +153,7 @@ def parse_appliance_uplinks(string_table: StringTable) -> Appliance | None: agent_section_cisco_meraki_org_appliance_uplinks = AgentSection( - name="cisco_meraki_org_appliance_uplinks", + name='cisco_meraki_org_appliance_uplinks', parse_function=parse_appliance_uplinks, ) @@ -163,10 +164,11 @@ def discover_appliance_uplinks(section: Appliance) -> DiscoveryResult: _STATUS_MAP = { - "active": 0, - "failed": 2, - "not_connected": 1, - "ready": 0, + 'active': 0, + 'failed': 2, + 'not_connected': 1, + 'ready': 0, + 'connecting': 1, } _TIMESPAN = 60 diff --git a/source/cmk_addons_plugins/meraki/agent_based/appliance_vpns.py b/source/cmk_addons_plugins/meraki/agent_based/appliance_vpns.py index b163e2e..38632f8 100644 --- a/source/cmk_addons_plugins/meraki/agent_based/appliance_vpns.py +++ b/source/cmk_addons_plugins/meraki/agent_based/appliance_vpns.py @@ -180,11 +180,11 @@ def check_appliance_vpns(item: str, params: Mapping[str, any], section: Mapping[ return None if peer.reachability is not None and peer.reachability.lower() in ['reachable']: - yield Result(state=State.OK, summary=f'{peer.reachability}') + yield Result(state=State.OK, summary=f'Status: {peer.reachability}') else: yield Result( state=State(params.get('status_not_reachable', 1)), - summary=f'{peer.reachability}', + summary=f'Status: {peer.reachability}', ) yield Result(state=State.OK, summary=f'Type: {peer.type}') diff --git a/source/cmk_addons_plugins/meraki/agent_based/cellular_uplinks.py b/source/cmk_addons_plugins/meraki/agent_based/cellular_uplinks.py index 2c9df38..c502b48 100644 --- a/source/cmk_addons_plugins/meraki/agent_based/cellular_uplinks.py +++ b/source/cmk_addons_plugins/meraki/agent_based/cellular_uplinks.py @@ -11,7 +11,7 @@ # 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" +# 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 @@ -35,39 +35,39 @@ from cmk_addons.plugins.meraki.lib.utils import get_int, load_json __cellular_uplinks = [ { - "highAvailability": { - "enabled": False, - "role": "primary" + 'highAvailability': { + 'enabled': False, + 'role': 'primary' }, - "lastReportedAt": "2023-11-13T19:52:06Z", - "model": "MG41", - "networkId": "L_575897802350012343", - "serial": "QQQQ-XXXX-ZZZZ", - "uplinks": [ + '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" + '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" + 'signalType': None, + 'status': 'active' } ] } ] -_LAST_REPORTED_AT = "%Y-%m-%dT%H:%M:%SZ" +_LAST_REPORTED_AT = '%Y-%m-%dT%H:%M:%SZ' @dataclass(frozen=True) @@ -159,7 +159,7 @@ def parse_cellular_uplinks(string_table: StringTable) -> CellularGateway | None: agent_section_cisco_meraki_org_cellular_uplinks = AgentSection( - name="cisco_meraki_org_cellular_uplinks", + name='cisco_meraki_org_cellular_uplinks', parse_function=parse_cellular_uplinks, ) diff --git a/source/cmk_addons_plugins/meraki/agent_based/switch_ports_statuses.py b/source/cmk_addons_plugins/meraki/agent_based/switch_ports_statuses.py index 7ec77bf..f210421 100644 --- a/source/cmk_addons_plugins/meraki/agent_based/switch_ports_statuses.py +++ b/source/cmk_addons_plugins/meraki/agent_based/switch_ports_statuses.py @@ -7,7 +7,7 @@ # URL : https://thl-cmk.hopto.org # Date : 2023-11-11 # File : switch_ports_statuses.py (check plugin) - +from Cython.Shadow import returns # 2024-04-08: moved neighbour_name and neighbour_port to key columns # 2024-04-27: made data parsing more robust # 2024-05-12: added support for MerakiGetOrganizationSwitchPortsStatusesBySwitch (Early Access) @@ -19,6 +19,14 @@ # 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 +# 2024-11-17: changed operational/admin status to up/down from connected/disconnected, enabled/disabled +# incompatible change item from "Port %s" to "Interface %s" -> (Port is moved to %s) +# -> rediscover your devices (tabularasa) +# added interface inventory +# added hostlabel function for nvdct/has_lldp_neighbours +# 2024-11-23 fixed crash on missing traffic data +# incompatible removed "Port" from item -> use only interface index +# 2024-12-14: reworked output of port status # ToDo: create service label cmk/meraki/uplink:yes/no @@ -39,6 +47,8 @@ from cmk.agent_based.v2 import ( TableRow, check_levels, render, + HostLabelGenerator, + HostLabel, ) from cmk_addons.plugins.meraki.lib.utils import get_float, get_int, load_json @@ -51,6 +61,13 @@ class SwitchSecurePort: configOverrides: Mapping[any] | None enabled: bool | None + __secure_port = { + "active": False, + "authenticationStatus": "Disabled", + "configOverrides": {}, + "enabled": False, + } + @classmethod def parse(cls, secure_port: Mapping[str, any] | None): return cls( @@ -70,14 +87,26 @@ class SwitchPortCDP: device_port: str | None platform: str | None version: str | None + native_vlan: str | None + + __cdp = { + "address": "172.24.10.1", + "capabilities": "Switch", + "deviceId": "149f43b14530", + "nativeVlan": 10, + "platform": "MS250-48FP", + "portId": "Port 49", + "version": "1", + } @classmethod def parse(cls, cdp: Mapping[str, str] | None): return cls( - device_id=str(cdp['deviceId']) if cdp.get('deviceId') is not None else None, - device_port=str(cdp['portId']) if cdp.get('portId') is not None else None, address=str(cdp['address']) if cdp.get('address') is not None else None, capabilities=str(cdp['capabilities']) if cdp.get('capabilities') is not None else None, + device_id=str(cdp['deviceId']) if cdp.get('deviceId') is not None else None, + device_port=str(cdp['portId']) if cdp.get('portId') is not None else None, + native_vlan=str(cdp['nativeVlan']) if cdp.get('nativeVlan') is not None else None, platform=str(cdp['platform']) if cdp.get('platform') is not None else None, version=str(cdp['version']) if cdp.get('version') is not None else None, ) if cdp else None @@ -93,24 +122,36 @@ class SwitchPortLLDP: system_description: str | None system_name: str | None + __lldp = { + "chassisId": "14:9f:43:b1:45:30", + "managementAddress": "172.24.10.1", + "managementVlan": 10, + "portDescription": "Port 49", + "portId": "49", + "portVlan": 10, + "systemCapabilities": "S-VLAN Component of a VLAN Bridge", + "systemDescription": "Meraki MS250-48FP Cloud Managed PoE Switch", + "systemName": "Meraki MS250-48FP - DV1-R005", + } + @classmethod def parse(cls, lldp: Mapping[str, str] | None): return cls( + cache_capabilities=str(lldp['systemCapabilities']) if lldp.get('systemCapabilities') is not None else None, chassis_id=str(lldp['chassisId']) if lldp.get('chassisId') is not None else None, + management_address=str(lldp['managementAddress']) if lldp.get('managementAddress') is not None else None, + port_description=str(lldp['portDescription']) if lldp.get('portDescription') is not None else None, port_id=str(lldp['portId']) if lldp.get('portId') is not None else None, - system_name=str(lldp['systemName']) if lldp.get('systemName') is not None else None, system_description=str(lldp['systemDescription']) if lldp.get('systemDescription') is not None else None, - port_description=str(lldp['portDescription']) if lldp.get('portDescription') is not None else None, - cache_capabilities=str(lldp['systemCapabilities']) if lldp.get('systemCapabilities') is not None else None, - management_address=str(lldp['managementAddress']) if lldp.get('managementAddress') is not None else None, + system_name=str(lldp['systemName']) if lldp.get('systemName') is not None else None, ) if lldp else None @dataclass(frozen=True) class SwitchPortUsage: - total: float - sent: float recv: float + sent: float + total: float @classmethod def parse(cls, usage: Mapping[str, float] | None): @@ -124,9 +165,9 @@ class SwitchPortUsage: """ return cls( - total=get_float(usage.get('total')) * 1000, - sent=get_float(usage.get('sent')) * 1000, recv=get_float(usage.get('recv')) * 1000, + sent=get_float(usage.get('sent')) * 1000, + total=get_float(usage.get('total')) * 1000, ) if usage else None @@ -148,10 +189,11 @@ class SwitchPortTraffic: Returns: """ + return cls( - total=get_float(traffic.get('total')) * 1000, - sent=get_float(traffic.get('sent')) * 1000, recv=get_float(traffic.get('recv')) * 1000, + sent=get_float(traffic.get('sent')) * 1000, + total=get_float(traffic.get('total')) * 1000, ) if traffic else None @@ -170,62 +212,92 @@ class SwitchPortSpanningTree: Returns: """ + if isinstance(spanning_tree, dict): return cls( status=[str(status) for status in spanning_tree.get('statuses', [])] ) +def parse_admin_state(admin_state: bool | None) -> str | None: + state_map = { + True: 1, + False: 2, + } + return state_map.get(admin_state) + + +def parse_operational_state(operational_state: str | None) -> str | None: + state_map = { + 'connected': 1, + 'disconnected': 2, + } + if isinstance(operational_state, str): + return state_map.get(operational_state.lower()) + + @dataclass(frozen=True) class SwitchPort: + port_id: int # needs to bee always there + admin_state: int | None cdp: SwitchPortCDP | None client_count: int | None duplex: str | None - enabled: bool | None errors: Sequence[str] | None is_up_link: bool | None lldp: SwitchPortLLDP | None - port_id: str | None + operational_state: int | None power_usage_in_wh: float | None secure_port: SwitchSecurePort | None + spanning_tree: SwitchPortSpanningTree | None speed: str | None - status: str | None traffic: SwitchPortTraffic | None usage: SwitchPortUsage | None warnings: Sequence[str] - spanning_tree: SwitchPortSpanningTree | None + # syntetic settings + alias: str | None + description: str | None + if_port_type: str | None + name: str | None @classmethod def parse(cls, port: Mapping[str, object]): return cls( - port_id=str(port['portId']) if port.get('portId') is not None else None, + port_id=int(port['portId']), # needs to be always there + admin_state=parse_admin_state(port.get('enabled')), + cdp=SwitchPortCDP.parse(port.get('cdp')), client_count=get_int(port.get('clientCount')), duplex=str(port['duplex']) if port.get('duplex') is not None else None, - enabled=bool(port['enabled']) if port.get('enabled') is not None else None, errors=port['errors'] if port.get('errors') is not None else None, is_up_link=bool(port['isUplink']) if port.get('isUplink') is not None else None, - power_usage_in_wh=get_float(port.get('powerUsageInWh')), - speed=str(port['speed']) if port.get('speed') is not None else None, - status=str(port['status']) if port.get('status') is not None else None, - warnings=port['warnings'] if port.get('warnings') is not None else None, lldp=SwitchPortLLDP.parse(port.get('lldp')), - cdp=SwitchPortCDP.parse(port.get('cdp')), + operational_state=parse_operational_state(port.get('status')), + power_usage_in_wh=get_float(port.get('powerUsageInWh')), secure_port=SwitchSecurePort.parse(port.get('securePort')), - usage=SwitchPortUsage.parse(port.get('usageInKb')), + spanning_tree=SwitchPortSpanningTree.parse(port.get('spanningTree')), + speed=str(port['speed']) if port.get('speed') is not None else None, traffic=SwitchPortTraffic.parse((port.get('trafficInKbps'))), - spanning_tree=SwitchPortSpanningTree.parse(port.get('spanningTree')) + usage=SwitchPortUsage.parse(port.get('usageInKb')), + warnings=port['warnings'] if port.get('warnings') is not None else None, + # synthetic settings + alias=f'Port {port["portId"]}' if port.get('portId') is not None else None, + description=f'Port {port["portId"]}' if port.get('portId') is not None else None, + if_port_type='6 - ethernetCsmacd', + name=f'Port {port["portId"]}' if port.get('portId') is not None else None, ) -_admin_status = { - True: 'enabled', - False: 'disabled', -} - -_is_up_link = { - True: 'yes', - False: 'no', -} +def host_label_meraki_switch_ports_statuses(section: Mapping[str, SwitchPort]) -> HostLabelGenerator: + """Host label function + Labels: + "nvdct/has_lldp_neighbours": + This label is set to "yes" for all hosts with LLDP neighbours + """ + for port in section.values(): + if port.lldp: + yield HostLabel(name="nvdct/has_lldp_neighbours", value="yes") + break + # only set LLDP label, Merkai CDP data are not usefully for NVDCT def parse_switch_ports_statuses(string_table: StringTable) -> Mapping[str, SwitchPort] | None: @@ -235,33 +307,32 @@ def parse_switch_ports_statuses(string_table: StringTable) -> Mapping[str, Switc if isinstance(json_data, dict) and 'ports' in json_data.keys(): json_data = json_data['ports'] - return {port['portId']: SwitchPort.parse(port) for port in json_data} + return {port["portId"]: SwitchPort.parse(port) for port in json_data if port.get('portId', '').isdigit()} agent_section_cisco_meraki_org_switch_ports_statuses = AgentSection( name="cisco_meraki_org_switch_ports_statuses", parse_function=parse_switch_ports_statuses, + host_label_function=host_label_meraki_switch_ports_statuses, ) def discover_switch_ports_statuses(params: Mapping[str, object], section: Mapping[str, SwitchPort]) -> DiscoveryResult: - discovered_port_states = params['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') + state_map = { + 1: 'up', + 2: 'down', + } + admin_port_states = params['admin_port_states'] + operational_port_states = params['operational_port_states'] - for port in section.values(): - if port.enabled in discovered_port_states and port.status.lower() in discovered_port_states: + for item, port in section.items(): + if state_map.get(port.admin_state) in admin_port_states and \ + state_map.get(port.operational_state) in operational_port_states: yield Service( - item=port.port_id, + item=item, parameters={ - 'enabled': port.enabled, - 'status': port.status, + 'admin_state': port.admin_state, + 'operational_state': port.operational_state, 'speed': port.speed, } ) @@ -272,82 +343,98 @@ def render_network_bandwidth_bits(value: int) -> str: 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: - is_state = is_state if is_state else 'N/A' - was_state = was_state if was_state else 'N/A' - yield Result(state=State(state), notice=f'{message}: from {was_state}, to {is_state}') + state_map = { + 1: 'up', + 2: 'down', + } + + is_up_link = { + True: 'yes', + False: 'no', + } + + def has_changed (is_state: int | str | None, was_state: int | str | None) -> bool: + if not is_state or not was_state: + # ignore if state is None -> meaning this change is expected. OP state down -> op -> speed None -> xxx + return False + + if is_state == was_state: + return False + + return True if (port := section.get(item)) is None: return - # check admin state changed - yield from _status_changed( - is_state=_admin_status[port.enabled], - was_state=_admin_status[params['enabled']], - message='Admin status changed', - state=params['state_admin_change'], - ) - - if port.enabled: # check admin sate - yield Result(state=State.OK, notice=f'Admin status: {_admin_status[port.enabled]}') - # check operational status changed - yield from _status_changed( - is_state=port.status.lower(), - was_state=params['status'].lower(), - message='Operational status changed', - state=params['state_op_change'] + if port.admin_state == 2: + yield Result( + state=State(params['state_disabled']), + summary=f'(admin {state_map.get(port.admin_state)})', + details=f'Admin status: {state_map.get(port.admin_state)}', ) - if port.status.lower() == 'connected': # check operational state - 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, - was_state=params['speed'], - message='Speed changed', - state=params['state_speed_change'] - ) - if params['speed'] == port.speed: # only if speed unchanged - yield Result(state=State.OK, summary=f'Speed: {port.speed}') - - if params.get('show_traffic'): - yield from check_levels( - value=port.traffic.recv, # Bits - label='In', - metric_name='if_in_bps', - 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'({port.status})', - details=f'Operational status: {port.status}' - ) else: + yield Result(state=State.OK, notice=f'Admin status: {state_map.get(port.admin_state)}') + + if has_changed(port.admin_state, params['admin_state']): + message = f'changed admin {state_map.get(params["admin_state"])} -> {state_map.get(port.admin_state)}' + yield Result(state=State(params['state_admin_change']), notice=message) + + if port.admin_state == 2: # down + return + + if port.operational_state == 2: yield Result( - state=State(params['state_disabled']), - summary=f'({_admin_status[port.enabled].title()})', - details=f'Admin status: {_admin_status[port.enabled].title()}', + state=State(params['state_not_connected']), + summary=f'({state_map.get(port.operational_state)})', + details=f'Operational status: {state_map.get(port.operational_state)}' + ) + else: + yield Result( + state=State.OK, + summary=f'({state_map.get(port.operational_state)})', + details=f'Operational status: {state_map.get(port.operational_state)}' ) + if has_changed(port.operational_state, params['operational_state']): + message = f'changed {state_map.get(params["operational_state"])} -> {state_map.get(port.operational_state)}' + yield Result(state=State(params['state_op_change']), summary=message) + + if port.operational_state == 2: + return + + yield Result(state=State.OK, summary=f'Speed: {port.speed}') + + if has_changed(port.speed, params['speed']): + message = f'changed {params["speed"]} -> {port.speed}' + yield Result(state=State(params['state_speed_change']), summary=message) + + + if params.get('show_traffic') and port.traffic: + yield from check_levels( + value=port.traffic.recv, # Bits + label='In', + metric_name='if_in_bps', + 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}') + if port.is_up_link: - yield Result(state=State.OK, summary='UP-Link', details=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]}') + yield Result(state=State.OK, notice=f'UP-Link: {is_up_link[port.is_up_link]}') if port.power_usage_in_wh: yield Result(state=State.OK, summary=f'Power usage: {port.power_usage_in_wh} Wh') @@ -368,7 +455,7 @@ def check_switch_ports_statuses(item: str, params: Mapping[str, any], section: M check_plugin_cisco_meraki_org_switch_ports_statuses = CheckPlugin( name='cisco_meraki_org_switch_ports_statuses', - service_name='Port %s', + service_name='Interface %s', discovery_function=discover_switch_ports_statuses, check_function=check_switch_ports_statuses, check_default_parameters={ @@ -382,11 +469,38 @@ check_plugin_cisco_meraki_org_switch_ports_statuses = CheckPlugin( check_ruleset_name='cisco_meraki_switch_ports_statuses', discovery_ruleset_name='discovery_cisco_meraki_switch_ports_statuses', discovery_default_parameters={ - 'discovered_port_states': ['admin_enabled', 'admin_disabled', 'connected', 'disconnected'] + 'admin_port_states': ['up', 'down'], + 'operational_port_states': ['up', 'down'], } ) +def inventory_meraki_interfaces(section: Mapping[str, SwitchPort]) -> InventoryResult: + for port in section.values(): + yield TableRow( + path=['networking', 'interfaces'], + key_columns={ + "index": port.port_id, + }, + inventory_columns={ + **({'alias': port.alias} if port.alias else {}), + **({'description': port.description} if port.description else {}), + **({'name': port.name} if port.name else {}), + **({'admin_status': port.admin_state} if port.admin_state else {}), + **({'oper_status': port.operational_state} if port.operational_state else {}), + **({'speed': port.speed} if port.speed else {}), + **({'if_port_type': port.if_port_type} if port.if_port_type else {}), + }, + ) + + +inventory_plugin_inv_meraki_interfaces = InventoryPlugin( + name='inv_meraki_interfaces', + sections=['cisco_meraki_org_switch_ports_statuses'], + inventory_function=inventory_meraki_interfaces, +) + + def inventory_meraki_cdp_cache(section: Mapping[str, SwitchPort]) -> InventoryResult: path = ['networking', 'cdp_cache', 'neighbours'] @@ -394,14 +508,16 @@ def inventory_meraki_cdp_cache(section: Mapping[str, SwitchPort]) -> InventoryRe if cdp := port.cdp: key_columns = { 'local_port': port.port_id, - 'neighbour_name': cdp.device_id, + 'neighbour_name': '', 'neighbour_port': cdp.device_port, } neighbour = { - 'neighbour_address': cdp.address, - 'platform_details': cdp.version, - 'platform': cdp.platform, - 'capabilities': cdp.capabilities + **({'capabilities': cdp.capabilities} if cdp.capabilities else {}), + **({'native_vlan': cdp.native_vlan} if cdp.native_vlan else {}), + **({'neighbour_address': cdp.address} if cdp.address else {}), + **({'neighbour_id': cdp.device_id} if cdp.device_id else {}), + **({'platform': cdp.platform} if cdp.platform else {}), + **({'version': cdp.version} if cdp.version else {}), } yield TableRow( path=path, @@ -428,11 +544,11 @@ def inventory_meraki_lldp_cache(section: Mapping[str, SwitchPort]) -> InventoryR 'neighbour_port': lldp.port_id, } neighbour = { - 'neighbour_id': lldp.chassis_id, - 'system_description': lldp.system_description, - 'port_description': lldp.port_description, - 'capabilities': lldp.cache_capabilities, - 'neighbour_address': lldp.management_address, + **({'capabilities': lldp.cache_capabilities} if lldp.cache_capabilities else {}), + **({'neighbour_address': lldp.management_address} if lldp.management_address else {}), + **({'neighbour_id': lldp.chassis_id} if lldp.chassis_id else {}), + **({'port_description': lldp.port_description} if lldp.port_description else {}), + **({'system_description': lldp.system_description} if lldp.system_description else {}), } yield TableRow( path=path, diff --git a/source/cmk_addons_plugins/meraki/agent_based/wireless_device_ssid_status.py b/source/cmk_addons_plugins/meraki/agent_based/wireless_device_ssid_status.py index 5b65d48..2710ccc 100644 --- a/source/cmk_addons_plugins/meraki/agent_based/wireless_device_ssid_status.py +++ b/source/cmk_addons_plugins/meraki/agent_based/wireless_device_ssid_status.py @@ -15,6 +15,7 @@ # 2024-07-13: fixed crash on missing metrics (device dormant) ThX to Leon Buhleier # 2024-08-07: fixed crash on missing power value (unit only) ThX to Leon Buhleier # 2024-09-12: fixed missing SSID 0 ThX to Andreas Doehler +# 2024-12-13: fixed crash if no valid data received (json_data = [[]]) from collections.abc import Mapping from dataclasses import dataclass @@ -64,7 +65,7 @@ class SSID: def parse_wireless_device_status(string_table: StringTable) -> Mapping[str, SSID] | None: json_data = load_json(string_table) - if (json_data := json_data[0]) is None: + if not (json_data := json_data[0]): return ssids = {} @@ -78,7 +79,6 @@ def parse_wireless_device_status(string_table: StringTable) -> Mapping[str, SSID item = str(ssid_number) + ' on band ' + row.get('band') ssids[item] = SSID.parse(row) - return ssids diff --git a/source/cmk_addons_plugins/meraki/agent_based/wireless_ethernet_statuses.py b/source/cmk_addons_plugins/meraki/agent_based/wireless_ethernet_statuses.py index 8ad1aa7..88cc8fe 100644 --- a/source/cmk_addons_plugins/meraki/agent_based/wireless_ethernet_statuses.py +++ b/source/cmk_addons_plugins/meraki/agent_based/wireless_ethernet_statuses.py @@ -12,6 +12,9 @@ # 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 +# 2024-11-17: incompatible change item from "Port %s" to "Interface %s" -> rediscover your devices +# added interface inventory +# 2024-12-12: fixed crash if speed is None # ToDo: create ruleset cisco_meraki_wireless_ethernet_statuses @@ -28,6 +31,9 @@ from cmk.agent_based.v2 import ( State, StringTable, render, + InventoryPlugin, + InventoryResult, + TableRow, ) from cmk_addons.plugins.meraki.lib.utils import get_int, load_json @@ -176,7 +182,7 @@ def check_wireless_ethernet_statuses( check_plugin_cisco_meraki_org_wireless_ethernet_statuses = CheckPlugin( name='cisco_meraki_org_wireless_ethernet_statuses', - service_name='Port %s', + service_name='Interface %s', discovery_function=discover_wireless_ethernet_statuses, check_function=check_wireless_ethernet_statuses, check_default_parameters={ @@ -187,3 +193,29 @@ check_plugin_cisco_meraki_org_wireless_ethernet_statuses = CheckPlugin( }, # check_ruleset_name='cisco_meraki_wireless_ethernet_statuses', ) + + +def inventory_meraki_wireless_ethernet(section: Mapping[str, WirelessEthernetPort]) -> InventoryResult: + for port in section.values(): + yield TableRow( + path=['networking', 'interfaces'], + key_columns={ + "index": port.name.split(' ')[-1], + }, + inventory_columns={ + 'alias': port.name, + 'description': port.name, + 'name': port.name, + 'admin_status': 1, + 'oper_status': 1, + **({'speed': render.nicspeed(port.speed)} if port.speed is not None else {}), + 'if_port_type': '6 - ethernetCsmacd', + }, + ) + + +inventory_plugin_inv_meraki_wireless_ethernet = InventoryPlugin( + name='inv_meraki_wireless_ethernet', + sections=['cisco_meraki_org_wireless_ethernet_statuses'], + inventory_function=inventory_meraki_wireless_ethernet, +) diff --git a/source/cmk_addons_plugins/meraki/lib/agent.py b/source/cmk_addons_plugins/meraki/lib/agent.py index ef80ffe..50516ca 100644 --- a/source/cmk_addons_plugins/meraki/lib/agent.py +++ b/source/cmk_addons_plugins/meraki/lib/agent.py @@ -40,14 +40,17 @@ # 2024-09-12: added version check for min. Meraki SDK version # 2024-09-15: fixed MerakiGetOrganizationSwitchPortsStatusesBySwitch -> return only list of switches # 2024-11-16: fixed crash on missing items in MerakiGetOrganizationSwitchPortsStatusesBySwitch (ThX to Stephan Bergfeld) +# 2024-11-23: added appliance port api call -> not yet active +# 2024-12-13: fixed crash in SwitchPortStatus if response has no data (>Response [503}>) # 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/ # ToDo: https://developer.cisco.com/meraki/api-v1/list-tunnels/ +# ToDo: https://developer.cisco.com/meraki/api-v1/get-organization-wireless-clients-overview-by-device/ # if the following is available (right now only with early access enabled) -# ToDO: https://developer.cisco.com/meraki/api-v1/get-organization-switch-ports-statuses-by-switch/ -# ToDo: https://developer.cisco.com/meraki/api-v1/get-organization-switch-ports-overview/ # (done) +# ToDO: https://developer.cisco.com/meraki/api-v1/get-organization-switch-ports-statuses-by-switch/ # (done) +# ToDo: https://developer.cisco.com/meraki/api-v1/get-organization-switch-ports-overview/ # TODo: https://developer.cisco.com/meraki/api-v1/get-organization-certificates/ # ToDo: https://developer.cisco.com/meraki/api-v1/api-reference-early-access-api-platform-configure-firmwareupgrades-get-network-firmware-upgrades/ @@ -62,6 +65,7 @@ from argparse import Namespace from collections.abc import Iterable, Iterator, Mapping, Sequence from dataclasses import dataclass from enum import auto, Enum +from json import JSONDecodeError from logging import getLogger from os import environ from pathlib import Path @@ -89,6 +93,7 @@ from cmk_addons.plugins.meraki.lib.utils import ( # parameter names _SEC_NAME_APPLIANCE_UPLINKS, + _SEC_NAME_APPLIANCE_PORTS, _SEC_NAME_APPLIANCE_UPLINKS_USAGE, _SEC_NAME_APPLIANCE_VPNS, _SEC_NAME_APPLIANCE_PERFORMANCE, @@ -125,53 +130,55 @@ from cmk_addons.plugins.meraki.lib.utils import ( _SEC_CACHE_SWITCH_PORTS_STATUSES, _SEC_CACHE_WIRELESS_DEVICE_STATUS, _SEC_CACHE_WIRELESS_ETHERNET_STATUSES, + _SEC_CACHE_APPLIANCE_PORTS, ) _MERAKI_SDK_MIN_VERSION: Final = '1.46.0' -_LOGGER = getLogger("agent_cisco_meraki") +_LOGGER = getLogger('agent_cisco_meraki') -_API_NAME_API: Final = "api" -_API_NAME_DEVICE_NAME: Final = "name" +_API_NAME_API: Final = 'api' +_API_NAME_DEVICE_NAME: Final = 'name' _API_NAME_DEVICE_PRODUCT_TYPE: Final = 'productType' -_API_NAME_DEVICE_SERIAL: Final = "serial" +_API_NAME_DEVICE_SERIAL: Final = 'serial' _API_NAME_DEVICE_TYPE_APPLIANCE: Final = 'appliance' _API_NAME_DEVICE_TYPE_CAMERA: Final = 'camera' _API_NAME_DEVICE_TYPE_CELLULAR: Final = 'cellularGateway' _API_NAME_DEVICE_TYPE_SENSOR: Final = 'sensor' _API_NAME_DEVICE_TYPE_SWITCH: Final = 'switch' _API_NAME_DEVICE_TYPE_WIRELESS: Final = 'wireless' -_API_NAME_ENABLED: Final = "enabled" +_API_NAME_ENABLED: Final = 'enabled' _API_NAME_NETWORK_ID: Final = 'networkId' -_API_NAME_ORGANISATION_ID: Final = "id" -_API_NAME_ORGANISATION_NAME: Final = "name" +_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 ("-" -> "_")? +# 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", - _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_APPLIANCE_UPLINKS: 'appliance_uplinks', + _SEC_NAME_APPLIANCE_UPLINKS_USAGE: 'appliance_uplinks_usage', + _SEC_NAME_APPLIANCE_PORTS: 'appliance_ports', + _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', # Early Access - _SEC_NAME_ORG_SWITCH_PORTS_STATUSES: "org_switch_ports_statuses", + _SEC_NAME_ORG_SWITCH_PORTS_STATUSES: 'org_switch_ports_statuses', } # _MIN_CACHE_INTERVAL = 300 # _RANDOM_CACHE_INTERVAL = 300 -MerakiCacheFilePath = Path(tmp_dir) / "agents" / "agent_cisco_meraki" +MerakiCacheFilePath = Path(tmp_dir) / 'agents' / 'agent_cisco_meraki' MerakiAPIData = Mapping[str, object] @@ -235,7 +242,7 @@ class Section: piggyback: str | None = None def get_name(self) -> str: - return "_".join(["cisco_meraki", self.api_data_source.name, self.name]) + return '_'.join(['cisco_meraki', self.api_data_source.name, self.name]) class _Organisation(TypedDict): @@ -258,13 +265,13 @@ class _Organisation(TypedDict): # # --\ DataCache # | -# |--\ MerakiSection +# +--\ MerakiSection # | - adds cache_interval = 86400 # | - adds get_validity_from_args = True # | -# |--> MerakiGetOrganizations -> default 86400 +# +--> MerakiGetOrganizations -> default 86400 # | -# |--\ MerakiSectionOrg +# +--\ MerakiSectionOrg # | | - adds org_id parameter # | | # | |--> MerakiGetOrganization -> default 86400 @@ -282,13 +289,20 @@ class _Organisation(TypedDict): # | |--> MerakiGetOrganizationCellularGatewayUplinkStatuses -> ex. 60. # | |--> MerakiGetOrganizationSwitchPortsStatusesBySwitch -> ex. 60+ # | -# |--\ MerakiSectionSerial -# | - adds serial as parameter -# | - sets cache_interval = 60 + randrange(300) +# +--\ MerakiSectionSerial +# | | - adds serial as parameter +# | | - sets cache_interval = 60 +# | | +# | |--> MerakiGetDeviceSwitchPortsStatuses -> default 60+ +# | |--> MerakiGetDeviceWirelessStatus -> default 60+ +# | |--> MerakiGetDeviceAppliancePerformance +# | +# +--\ MerakiSectionNetwork +# | - adds network id as parameter +# | - sets cache_interval = 60 # | -# |--> MerakiGetDeviceSwitchPortsStatuses -> default 60+ -# |--> MerakiGetDeviceWirelessStatus -> default 60+ -# |--> MerakiGetDeviceAppliancePerformance +# |--> MerakiGetNetworkAppliancePorts +# class MerakiSection(DataCache): def __init__( @@ -305,7 +319,7 @@ class MerakiSection(DataCache): @property def name(self): - return "meraki_section" + return 'meraki_section' @property def cache_interval(self): @@ -338,18 +352,29 @@ class MerakiSectionSerial(MerakiSection): super().__init__(config=config, cache_interval=cache_interval) +class MerakiSectionNetwork(MerakiSection): + def __init__( + self, + config: MerakiConfig, + network_id: str, + cache_interval: int = 1, + ): + self._network_id = network_id + super().__init__(config=config, cache_interval=cache_interval) + + class MerakiGetOrganizations(MerakiSection): @property def name(self): - return "getOrganizations" + return 'getOrganizations' def get_live_data(self): try: return self._config.dashboard.organizations.getOrganizations( - total_pages="all", + total_pages='all', ) except meraki.exceptions.APIError as e: - _LOGGER.debug("Get organisations: %r", e) + _LOGGER.debug('Get organisations: %r', e) return [] @@ -379,9 +404,9 @@ class MerakiGetOrganizationApiRequestsOverviewResponseCodesByInterval(MerakiSect # ) return self._config.dashboard.organizations.getOrganizationApiRequestsOverviewResponseCodesByInterval( self._org_id, - total_pages="all", - t0=strftime("%Y-%m-%dT%H:%M:%MZ", gmtime(now_time()-120)), - t1=strftime("%Y-%m-%dT%H:%M:%MZ", gmtime()) + total_pages='all', + t0=strftime('%Y-%m-%dT%H:%M:%MZ', gmtime(now_time()-120)), + t1=strftime('%Y-%m-%dT%H:%M:%MZ', gmtime()) ) except meraki.APIError as e: @@ -400,7 +425,7 @@ class MerakiGetOrganizationLicensesOverview(MerakiSectionOrg): self._org_id, ) except meraki.exceptions.APIError as e: - _LOGGER.debug("Organisation ID: %r: Get license overview: %r", self._org_id, e) + _LOGGER.debug('Organisation ID: %r: Get license overview: %r', self._org_id, e) return [] @@ -413,10 +438,10 @@ class MerakiGetOrganizationDevices(MerakiSectionOrg): try: return self._config.dashboard.organizations.getOrganizationDevices( self._org_id, - total_pages="all", + total_pages='all', ) except meraki.exceptions.APIError as e: - _LOGGER.debug("Organisation ID: %r: Get devices: %r", self._org_id, e) + _LOGGER.debug('Organisation ID: %r: Get devices: %r', self._org_id, e) return {} @@ -429,10 +454,10 @@ class MerakiGetOrganizationNetworks(MerakiSectionOrg): try: return self._config.dashboard.organizations.getOrganizationNetworks( self._org_id, - total_pages="all", + total_pages='all', ) except meraki.exceptions.APIError as e: - _LOGGER.debug("Organisation ID: %r: Get networks: %r", self._org_id, e) + _LOGGER.debug('Organisation ID: %r: Get networks: %r', self._org_id, e) return [] @@ -445,10 +470,10 @@ class MerakiGetOrganizationDevicesStatuses(MerakiSectionOrg): try: return self._config.dashboard.organizations.getOrganizationDevicesStatuses( self._org_id, - total_pages="all", + total_pages='all', ) except meraki.exceptions.APIError as e: - _LOGGER.debug("Organisation ID: %r: Get device statuses: %r", self._org_id, e) + _LOGGER.debug('Organisation ID: %r: Get device statuses: %r', self._org_id, e) return [] @@ -461,10 +486,10 @@ class MerakiGetOrganizationDevicesUplinksAddressesByDevice(MerakiSectionOrg): try: return self._config.dashboard.organizations.getOrganizationDevicesUplinksAddressesByDevice( self._org_id, - total_pages="all", + total_pages='all', ) except meraki.exceptions.APIError as e: - _LOGGER.debug("Organisation ID: %r: Get device statuses: %r", self._org_id, e) + _LOGGER.debug('Organisation ID: %r: Get device statuses: %r', self._org_id, e) return [] @@ -477,10 +502,10 @@ class MerakiGetOrganizationApplianceUplinkStatuses(MerakiSectionOrg): try: return self._config.dashboard.appliance.getOrganizationApplianceUplinkStatuses( self._org_id, - total_pages="all", + total_pages='all', ) except meraki.exceptions.APIError as e: - _LOGGER.debug("Organisation ID: %r: Get Appliance uplink status by network: %r", self._org_id, e) + _LOGGER.debug('Organisation ID: %r: Get Appliance uplink status by network: %r', self._org_id, e) return [] @@ -490,17 +515,17 @@ class MerakiGetOrganizationApplianceUplinksUsageByNetwork(MerakiSectionOrg): return f'getOrganizationApplianceUplinksUsageByNetwork_{self._org_id}' def get_live_data(self): - if meraki.__version__ < "1.39.0": + if meraki.__version__ < '1.39.0': _LOGGER.debug(f'Meraki SDK is to old. Installed: {meraki.__version__}, excepted: 1.39.0') return [] try: return self._config.dashboard.appliance.getOrganizationApplianceUplinksUsageByNetwork( organizationId=self._org_id, - total_pages="all", + total_pages='all', timespan=60 # default=86400 (one day), maximum=1209600 (14 days), needs to match value in check ) except meraki.exceptions.APIError as e: - _LOGGER.debug("Organisation ID: %r: Get Appliance uplink usage by network: %r", self._org_id, e) + _LOGGER.debug('Organisation ID: %r: Get Appliance uplink usage by network: %r', self._org_id, e) return [] @@ -513,11 +538,11 @@ class MerakiGetOrganizationApplianceVpnStatuses(MerakiSectionOrg): try: return self._config.dashboard.appliance.getOrganizationApplianceVpnStatuses( self._org_id, - total_pages="all", + total_pages='all', ) except meraki.exceptions.APIError as e: - _LOGGER.debug("Organisation ID: %r: Get Appliance VPN status by network: %r", self._org_id, e) + _LOGGER.debug('Organisation ID: %r: Get Appliance VPN status by network: %r', self._org_id, e) return [] @@ -532,7 +557,7 @@ class MerakiGetDeviceAppliancePerformance(MerakiSectionSerial): return self._config.dashboard.appliance.getDeviceAppliancePerformance( self._serial) except meraki.exceptions.APIError as e: - _LOGGER.debug("Serial: %r: Get appliance device performance: %r", + _LOGGER.debug('Serial: %r: Get appliance device performance: %r', self._serial, e) return [] @@ -546,10 +571,10 @@ class MerakiGetOrganizationSensorReadingsLatest(MerakiSectionOrg): try: return self._config.dashboard.sensor.getOrganizationSensorReadingsLatest( self._org_id, - total_pages="all", + total_pages='all', ) except meraki.exceptions.APIError as e: - _LOGGER.debug("Organisation ID: %r: Get sensor readings: %r", self._org_id, e) + _LOGGER.debug('Organisation ID: %r: Get sensor readings: %r', self._org_id, e) return [] @@ -562,11 +587,11 @@ class MerakiGetDeviceSwitchPortsStatuses(MerakiSectionSerial): try: return self._config.dashboard.switch.getDeviceSwitchPortsStatuses( self._serial, - # total_pages="all", + # total_pages='all', timespan=max(self._config.timespan, 900), ) except meraki.exceptions.APIError as e: - _LOGGER.debug("Serial: %r: Get Switch Port Statuses: %r", self._serial, e) + _LOGGER.debug('Serial: %r: Get Switch Port Statuses: %r', self._serial, e) return [] @@ -576,16 +601,16 @@ class MerakiGetOrganizationWirelessDevicesEthernetStatuses(MerakiSectionOrg): return f'getOrganizationWirelessDevicesEthernetStatuses_{self._org_id}' def get_live_data(self): - if meraki.__version__ < "1.39.0": + if meraki.__version__ < '1.39.0': _LOGGER.debug(f'Meraki SDK is to old. Installed: {meraki.__version__}, expceted: 1.39.0') return [] try: return self._config.dashboard.wireless.getOrganizationWirelessDevicesEthernetStatuses( self._org_id, - total_pages="all", + total_pages='all', ) except meraki.exceptions.APIError as e: - _LOGGER.debug("Organisation ID: %r: Get wireless devices ethernet statuses: %r", self._org_id, e) + _LOGGER.debug('Organisation ID: %r: Get wireless devices ethernet statuses: %r', self._org_id, e) return [] @@ -598,7 +623,7 @@ class MerakiGetDeviceWirelessStatus(MerakiSectionSerial): try: return self._config.dashboard.wireless.getDeviceWirelessStatus(self._serial) except meraki.exceptions.APIError as e: - _LOGGER.debug("Serial: %r: Get wireless device status: %r", self._serial, e) + _LOGGER.debug('Serial: %r: Get wireless device status: %r', self._serial, e) return [] @@ -611,7 +636,7 @@ class MerakiGetOrganizationCellularGatewayUplinkStatuses(MerakiSectionOrg): try: return self._config.dashboard.cellularGateway.getOrganizationCellularGatewayUplinkStatuses( self._org_id, - total_pages="all", + total_pages='all', ) except meraki.exceptions.APIError as e: _LOGGER.debug('Organisation ID: %r: Get cellular gateways uplink statuses: %r', self._org_id, e) @@ -652,12 +677,32 @@ class MerakiGetOrganizationSwitchPortsStatusesBySwitch(MerakiSectionOrg): except RequestException as e: _LOGGER.debug('Organisation ID: %r: Get Ports statuses by switch: %r', self._org_id, e) return [] - _response = response.json() + try: + _response = response.json() + except JSONDecodeError: + _LOGGER.debug('Organisation ID: %r: Get Ports statuses by switch: %r', self._org_id, response) + return [] if _response: return _response.get('items', []) return [] +class MerakiGetNetworkAppliancePorts(MerakiSectionNetwork): + @property + def name(self): + return f'getNetworkAppliancePorts_{self._network_id}' + + def get_live_data(self): + try: + return self._config.dashboard.appliance.getNetworkAppliancePorts( + self._network_id, + # total_pages='all', + ) + except meraki.exceptions.APIError as e: + _LOGGER.debug('Network ID: %r: Get appliance ports: %r', self._network_id, e) + return [] + + # # Main run # @@ -737,7 +782,7 @@ class MerakiOrganisation: device_piggyback = device[_API_NAME_DEVICE_NAME] except KeyError as e: _LOGGER.debug( - "Organisation ID: %r: Get device piggyback: %r", self.organisation_id, e + 'Organisation ID: %r: Get device piggyback: %r', self.organisation_id, e ) continue @@ -872,6 +917,60 @@ class MerakiOrganisation: piggyback=self._adjust_piggyback(host=piggyback), ) + # if _SEC_NAME_APPLIANCE_PORTS not in self.config.excluded_sections and networks: + # for network in networks: + # appliance_ports_by_network = MerakiGetNetworkAppliancePorts( + # config=self.config, + # network_id=network.get('id'), + # cache_interval=30, + # ).get_data(use_cache=self.config.use_cache) + # __ports = [ + # { + # 'number': 5, + # 'enabled': True, + # 'type': 'trunk', + # 'dropUntaggedTraffic': True, + # 'allowedVlans': 'all' + # }, + # { + # 'number': 6, + # 'enabled': True, + # 'type': 'trunk', + # 'dropUntaggedTraffic': True, + # 'allowedVlans': 'all' + # }, + # { + # 'number': 7, + # 'enabled': True, + # 'type': 'trunk', + # 'dropUntaggedTraffic': False, + # 'vlan': 1, + # 'allowedVlans': 'all'}, + # { + # 'number': 8, + # 'enabled': True, + # 'type': 'trunk', + # 'dropUntaggedTraffic': True, + # 'allowedVlans': 'all' + # }, + # { + # 'number': 9, + # 'enabled': True, + # 'type': 'trunk', + # 'dropUntaggedTraffic': False, + # 'vlan': 10, + # 'allowedVlans': 'all' + # }, + # { + # 'number': 10, + # 'enabled': True, + # 'type': 'trunk', + # 'dropUntaggedTraffic': False, + # 'vlan': 10, + # 'allowedVlans': 'all' + # } + # ] + 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]: @@ -982,8 +1081,8 @@ class MerakiOrganisation: return None licenses_overview.update( { - "organisation_id": self.organisation_id, - "organisation_name": self.organisation_name, + 'organisation_id': self.organisation_id, + 'organisation_name': self.organisation_name, } ) return licenses_overview @@ -1000,8 +1099,8 @@ class MerakiOrganisation: def _update_device(device: dict[str, object]) -> MerakiAPIData: device.update( { - "organisation_id": self.organisation_id, - "organisation_name": self.organisation_name, + 'organisation_id': self.organisation_id, + 'organisation_name': self.organisation_name, 'network_name': self._networks.get(device.get(_API_NAME_NETWORK_ID)).name, } ) @@ -1027,7 +1126,7 @@ class MerakiOrganisation: _LOGGER.debug(f'Host without name _get_device_piggyback serial: {serial}') return None except KeyError as e: - _LOGGER.debug("Organisation ID: %r: Get device piggyback: %r", self.organisation_id, e) + _LOGGER.debug('Organisation ID: %r: Get device piggyback: %r', self.organisation_id, e) return None @staticmethod @@ -1076,8 +1175,9 @@ def _write_sections(sections: Iterable[Section]) -> None: @dataclass(frozen=True) class CachePerSection: appliance_performance: int - appliance_uplinks_usage: int + appliance_ports: int appliance_uplinks: int + appliance_uplinks_usage: int appliance_vpns: int cellular_uplinks: int device_info: int @@ -1110,39 +1210,39 @@ class Args(Namespace): def parse_arguments(argv: Sequence[str] | None) -> Args: parser = create_default_argument_parser(description=__doc__) - parser.add_argument("hostname") + parser.add_argument('hostname') parser.add_argument( - "apikey", - help="API key for the Meraki API dashboard access.", + 'apikey', + help='API key for the Meraki API dashboard access.', ) - parser.add_argument("--proxy", type=str) + parser.add_argument('--proxy', type=str) # parser.add_argument( - # "--sections", - # nargs="+", + # '--sections', + # nargs='+', # choices=list(_SECTION_NAME_MAP), # default=list(_SECTION_NAME_MAP), - # help="Explicit sections that are collected.", + # help='Explicit sections that are collected.', # ) parser.add_argument( - "--excluded-sections", - nargs="*", + '--excluded-sections', + nargs='*', choices=list(_SECTION_NAME_MAP), default=[], - help="Sections that are excluded form data collected.", + help='Sections that are excluded form data collected.', ) parser.add_argument( - "--orgs", - nargs="+", + '--orgs', + nargs='+', default=[], - help="Explicit organisation IDs that are checked.", + help='Explicit organisation IDs that are checked.', ) # parser.add_argument( - # "--prefix-suffix", + # '--prefix-suffix', # nargs=5, # action='append', # default=[], @@ -1168,13 +1268,14 @@ def parse_arguments(argv: Sequence[str] | None) -> Args: parser.add_argument( '--cache-per-section', - nargs="+", + nargs='+', type=int, - help="List of cache time per section in minutes", + help='List of cache time per section in minutes', default=[ _SEC_CACHE_APPLIANCE_PERFORMANCE, - _SEC_CACHE_APPLIANCE_UPLINKS_USAGE, + _SEC_CACHE_APPLIANCE_PORTS, _SEC_CACHE_APPLIANCE_UPLINKS, + _SEC_CACHE_APPLIANCE_UPLINKS_USAGE, _SEC_CACHE_APPLIANCE_VPNS, _SEC_CACHE_CELLULAR_UPLINKS, _SEC_CACHE_DEVICE_INFO, @@ -1182,13 +1283,13 @@ def parse_arguments(argv: Sequence[str] | None) -> Args: _SEC_CACHE_DEVICE_UPLINKS_INFO, _SEC_CACHE_LICENSES_OVERVIEW, _SEC_CACHE_NETWORKS, + _SEC_CACHE_ORGANISATIONS, _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_CACHE_WIRELESS_ETHERNET_STATUSES, ] ) diff --git a/source/cmk_addons_plugins/meraki/lib/utils.py b/source/cmk_addons_plugins/meraki/lib/utils.py index 810af86..2301c95 100644 --- a/source/cmk_addons_plugins/meraki/lib/utils.py +++ b/source/cmk_addons_plugins/meraki/lib/utils.py @@ -23,23 +23,24 @@ 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 -_SEC_NAME_ORG_API_REQUESTS: Final = "api-requests-by-organization" # internal use, runs always - -_SEC_NAME_APPLIANCE_UPLINKS: Final = "appliance-uplinks" -_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-status" -_SEC_NAME_DEVICE_UPLINKS_INFO: Final = "device-uplinks-info" -_SEC_NAME_LICENSES_OVERVIEW: Final = "licenses-overview" -_SEC_NAME_SENSOR_READINGS: Final = "sensor-readings" -_SEC_NAME_SWITCH_PORTS_STATUSES: Final = "switch-ports-statuses" -_SEC_NAME_WIRELESS_DEVICE_STATUS: Final = "wireless-device-status" -_SEC_NAME_WIRELESS_ETHERNET_STATUSES: Final = "wireless-ethernet-statuses" +_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 +_SEC_NAME_ORG_API_REQUESTS: Final = 'api-requests-by-organization' # internal use, runs always + +_SEC_NAME_APPLIANCE_UPLINKS: Final = 'appliance-uplinks' +_SEC_NAME_APPLIANCE_PORTS: Final = 'appliance-ports' +_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-status' +_SEC_NAME_DEVICE_UPLINKS_INFO: Final = 'device-uplinks-info' +_SEC_NAME_LICENSES_OVERVIEW: Final = 'licenses-overview' +_SEC_NAME_SENSOR_READINGS: Final = 'sensor-readings' +_SEC_NAME_SWITCH_PORTS_STATUSES: Final = 'switch-ports-statuses' +_SEC_NAME_WIRELESS_DEVICE_STATUS: Final = 'wireless-device-status' +_SEC_NAME_WIRELESS_ETHERNET_STATUSES: Final = 'wireless-ethernet-statuses' # api cache defaults per section @@ -60,22 +61,21 @@ _SEC_CACHE_SENSOR_READINGS = 0 _SEC_CACHE_SWITCH_PORTS_STATUSES = 0 _SEC_CACHE_WIRELESS_DEVICE_STATUS = 30 _SEC_CACHE_WIRELESS_ETHERNET_STATUSES = 30 - - +_SEC_CACHE_APPLIANCE_PORTS = 30 # Early Access -_SEC_NAME_ORG_SWITCH_PORTS_STATUSES: Final = "org-switch-ports-statuses" +_SEC_NAME_ORG_SWITCH_PORTS_STATUSES: Final = 'org-switch-ports-statuses' @dataclass(frozen=True) class MerakiNetwork: - id: str # "N_24329156", - name: str # "Main Office", - product_types: Sequence[str] # ["appliance", "switch", "wireless"] - time_zone: str # "America/Los_Angeles", - tags: Sequence[str] # [ "tag1", "tag2" ], - enrollment_string: str | None # "my-enrollment-string", - notes: str # "Additional description of the network", + id: str # 'N_24329156', + name: str # 'Main Office', + product_types: Sequence[str] # ['appliance', 'switch', 'wireless'] + time_zone: str # 'America/Los_Angeles', + tags: Sequence[str] # [ 'tag1', 'tag2' ], + enrollment_string: str | None # 'my-enrollment-string', + notes: str # 'Additional description of the network', is_bound_to_config_template: bool # false organisation_id: str organisation_name: str @@ -97,7 +97,7 @@ def check_last_reported_ts( if (age := time.time() - last_reported_ts) < 0: yield Result( state=State.OK, - summary="Negative timespan since last report time.", + summary='Negative timespan since last report time.', ) return if levels_upper: diff --git a/source/cmk_addons_plugins/meraki/rulesets/switch_ports_statuses.py b/source/cmk_addons_plugins/meraki/rulesets/switch_ports_statuses.py index 3cea6f7..5c2f580 100644 --- a/source/cmk_addons_plugins/meraki/rulesets/switch_ports_statuses.py +++ b/source/cmk_addons_plugins/meraki/rulesets/switch_ports_statuses.py @@ -15,6 +15,10 @@ # 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 +# 2024-11-17: incompatible change to match changed port status check -> recreate your discovery rule +# 2024-11-23: added missing discovery parameters admin_state and operational_state +# removed discovery parameters 'enabled' and 'status' +# reference to section organization_switch_ports removed, missing traffic, lldp, cdp, stp, ... from cmk.rulesets.v1 import Label, Title, Help from cmk.rulesets.v1.form_specs import ( @@ -29,11 +33,6 @@ from cmk.rulesets.v1.form_specs import ( ) 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( @@ -55,17 +54,17 @@ def _parameter_form(): )), 'state_speed_change': DictElement( parameter_form=ServiceState( - title=Title('Monitoring state if speed is changed'), + title=Title('Monitoring state if speed has changed'), prefill=DefaultValue(ServiceState.WARN), )), 'state_admin_change': DictElement( parameter_form=ServiceState( - title=Title('Monitoring state if admin state is changed'), + title=Title('Monitoring state if admin state has changed'), prefill=DefaultValue(ServiceState.WARN), )), 'state_op_change': DictElement( parameter_form=ServiceState( - title=Title('Monitoring state if operational state is changed'), + title=Title('Monitoring state if operational state has changed'), prefill=DefaultValue(ServiceState.WARN), )), 'show_traffic': DictElement( @@ -76,23 +75,20 @@ def _parameter_form(): 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.' + 'this will exceeds the limits of the allowed API requests per second.' ), )), # params from discovery - 'enabled': DictElement( + 'admin_state': DictElement( render_only=True, parameter_form=String( title=Title('Discovered admin state') - )), - 'status': DictElement( + ) + ), + 'operational_state': DictElement( render_only=True, parameter_form=String( - title=Title('Discovered status') + title=Title('Discovered operational state') ) ), 'speed': DictElement( @@ -100,7 +96,7 @@ def _parameter_form(): parameter_form=String( title=Title('Discovered speed') ) - ) + ), }, ) @@ -117,33 +113,42 @@ rule_spec_cisco_meraki_switch_ports_statuses = CheckParameters( def _discovery_form(): return Dictionary( elements={ - 'discovered_port_states': DictElement( + 'operational_port_states': DictElement( parameter_form=MultipleChoice( - title=Title('Select Ports to discover'), + title=Title('Match port states'), elements=[ MultipleChoiceElement( - title=Title('Admin enabled'), - name='admin_enabled', + title=Title('1 - up'), + name='up', ), MultipleChoiceElement( - title=Title('Admin disabled'), - name='admin_disabled', + title=Title('2 - down'), + name='down', ), + ], + help_text=Help('Apply this rule only to interfaces whose port state is listed below.'), + prefill=DefaultValue([ + 'up', + 'down', + ]) + )), + 'admin_port_states': DictElement( + parameter_form=MultipleChoice( + title=Title('Match admin states'), + elements=[ MultipleChoiceElement( - title=Title('Connected'), - name='connected', + title=Title('1 - up'), + name='up', ), MultipleChoiceElement( - title=Title('Disconnected'), - name='disconnected', + title=Title('2 - down'), + name='down', ), ], - help_text=Help('Select the port states for discovery'), + help_text=Help('Apply this rule only to interfaces whose admin state is listed below'), prefill=DefaultValue([ - 'admin_enabled', - 'admin_disabled', - 'connected', - 'disconnected', + 'up', + 'down', ]) )), }, diff --git a/source/packages/cisco_meraki b/source/packages/cisco_meraki index 21fb4c5..0687aaf 100644 --- a/source/packages/cisco_meraki +++ b/source/packages/cisco_meraki @@ -63,7 +63,7 @@ 'web': ['plugins/views/cisco_meraki.py']}, 'name': 'cisco_meraki', 'title': 'Cisco Meraki special agent', - 'version': '1.3.7-20241116', + 'version': '1.4.1-20241217', 'version.min_required': '2.3.0b1', 'version.packaged': 'cmk-mkp-tool 0.2.0', 'version.usable_until': '2.4.0b1'} -- GitLab