diff --git a/README.md b/README.md index 0cc147b81e952e6a19d36ee3632d54a23bd11d6c..20dce74051d6801983b6b7a7eb37b94818829cdc 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 Binary files /dev/null and b/mkp/cisco_meraki-1.4.1-20241217.mkp differ diff --git a/source/agent_based/cisco_meraki_org_device_status.py b/source/agent_based/cisco_meraki_org_device_status.py index 56b1c6feff8c3d562e4fd8c93c704a8ac9e0efc5..5a0d4ac111013d1a1b06a0211ddaf790aa198f95 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 7a093bf6ffadf5e3bc95ff813d5fe1c005eb2343..4fb45ed029fd10366be670aa12b0a51ef439bd4c 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 b163e2e8973d3df7a15e16bae1747ca9f8031855..38632f82eababbe95b45605ecd888125ca98b408 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 2c9df386bac7c2b34713248dab4c659d6841fe95..c502b480df116d9fbf4992843c9e3d85e17bfd84 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 7ec77bfc4a5903b9ee28c1f46faa0319c2f5f9c0..f210421271e57701d52f4976a163ca19ee1a394b 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 5b65d48d91319810eb83b6be1a01f3f714ddb20a..2710ccccbd6bd41ff8bf172d9760b166270fb76f 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 8ad1aa7c6b3233bb87574750ba832da63618226c..88cc8fefc76a256d2a5cf293aa6d37e8288056aa 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 ef80ffec40072310c8b5ce29ed392d08b0f2b4b1..50516cab9144fb91b7938b72da60eacc39b6a532 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 810af8679a6934857233ada91dab85ca4687af15..2301c95996f5a4436484f3db6aa791417910c34b 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 3cea6f758399ab1aae5ec9efe18d8f6ed2f252f3..5c2f580a360e54d38831d46a41962e42c4f4c6de 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 21fb4c5e2211b498062e91125b9a9870cd1249f2..0687aaf2b189236c125c5ba846f8038ebc1de7f5 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'}