Collection of CheckMK checks (see https://checkmk.com/). All checks and plugins are provided as is. Absolutely no warranty. Send any comments to thl-cmk[at]outlook[dot]com

Skip to content
Snippets Groups Projects
Commit 613759d4 authored by thl-cmk's avatar thl-cmk :flag_na:
Browse files

update project

parent c7641616
No related branches found
No related tags found
No related merge requests found
Showing
with 606 additions and 350 deletions
[PACKAGE]: ../../raw/master/mkp/cisco_meraki-1.2.12-20240512.mkp "cisco_meraki-1.2.12-20240512.mkp"
[PACKAGE]: ../../raw/master/mkp/cisco_meraki-1.2.14-20240521.mkp "cisco_meraki-1.2.14-20240521.mkp"
[SDK]: ../../raw/master/mkp/MerakiSDK-1.46.0-20240516.mkp "MerakiSDK-1.46.0-20240516.mkpp"
# Cisco Meraki special agent
......
File added
......@@ -11,17 +11,20 @@
# 2023-11-12: re-added appliance uplinks usage
# 2024-04-27: fixed crash on missing last reported time (THX to Norman Kühnberger)
# made data parsing more robust
# 2024-05-15: fixed typo in output of uplink.received (in -> In) (ThX to Rickard Eriksson)
# moved parse function to the dataclasses
# 2025-05-19: reworked appliance uplinks usage
from dataclasses import dataclass
from datetime import datetime
from _collections_abc import Mapping
from cmk.base.plugins.agent_based.agent_based_api.v1 import (
register,
Result,
Service,
State,
Metric,
check_levels,
register,
render,
)
from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import (
......@@ -30,43 +33,10 @@ from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import (
StringTable,
)
from cmk.base.plugins.agent_based.utils.cisco_meraki import (
get_int, # type: ignore[import]
load_json,
get_int,
)
_LAST_REPORTED_AT = "%Y-%m-%dT%H:%M:%SZ"
@dataclass(frozen=True)
class ApplianceUplink:
interface: str | None
gateway: str | None
ip: str | None
ip_assigned_by: str | None
primary_dns: str | None
secondary_dns: str | None
status: str | None
public_ip: str | None
sent: int | None
received: int | None
@dataclass(frozen=True)
class ApplianceUplinkHA:
enabled: bool | None
role: str | None
@dataclass(frozen=True)
class Appliance:
serial: str | None
model: str | None
last_reported_at: datetime | None
network_name: str | None
uplinks: Mapping[str, ApplianceUplink] | None
high_availability: ApplianceUplinkHA | None
# sample string_table
__appliance_uplinks = [
{
......@@ -86,11 +56,11 @@ __appliance_uplinks = [
'ip': '192.168.10.2',
'ipAssignedBy': 'static',
'primaryDns': '192.168.10.1',
'publicIp': '79.197.135.251',
'publicIp': '20.197.135.251',
'secondaryDns': '9.9.9.9',
'status': 'active',
"received": 52320006,
"sent": 52928038,
"received": 52320006, # bytes
"sent": 52928038, # bytes
},
{
'gateway': '192.168.5.100',
......@@ -98,7 +68,7 @@ __appliance_uplinks = [
'ip': '192.168.5.236',
'ipAssignedBy': 'dhcp',
'primaryDns': '192.168.5.100',
'publicIp': '84.151.100.109',
'publicIp': '20.151.100.109',
'secondaryDns': '0.0.0.0',
'status': 'active'
}
......@@ -106,38 +76,77 @@ __appliance_uplinks = [
}
]
_LAST_REPORTED_AT = "%Y-%m-%dT%H:%M:%SZ"
@dataclass(frozen=True)
class ApplianceUplink:
gateway: str | None
interface: str | None
ip: str | None
ip_assigned_by: str | None
primary_dns: str | None
public_ip: str | None
received: int | None
secondary_dns: str | None
sent: int | None
status: str | None
@classmethod
def parse(cls, uplink: Mapping[str: object]):
return cls(
gateway=str(uplink['gateway']) if uplink.get('gateway') is not None else None,
interface=str(uplink['interface']) if uplink.get('interface') is not None else None,
ip=str(uplink['ip']) if uplink.get('ip') is not None else None,
ip_assigned_by=str(uplink['ipAssignedBy']) if uplink.get('ipAssignedBy') is not None else None,
primary_dns=str(uplink['primaryDns']) if uplink.get('primaryDns') is not None else None,
public_ip=str(uplink['publicIp']) if uplink.get('publicIp') is not None else None,
received=get_int(uplink.get('received')),
secondary_dns=str(uplink['secondaryDns']) if uplink.get('secondaryDns') is not None else None,
sent=get_int(uplink.get('sent')),
status=str(uplink['status']) if uplink.get('status') is not None else None,
)
@dataclass(frozen=True)
class ApplianceUplinkHA:
enabled: bool | None
role: str | None
@classmethod
def parse(cls, high_availability: Mapping[str: object]):
return cls(
enabled=bool(high_availability['enabled']) if high_availability.get('enabled') is not None else None,
role=str(high_availability['role']) if high_availability.get('role') is not None else None,
)
@dataclass(frozen=True)
class Appliance:
high_availability: ApplianceUplinkHA | None
last_reported_at: datetime | None
model: str | None
network_name: str | None
serial: str | None
uplinks: Mapping[str, ApplianceUplink] | None
@classmethod
def parse(cls, appliance: Mapping[str: object]):
return cls(
high_availability=ApplianceUplinkHA.parse(appliance['highAvailability']) if appliance.get(
'highAvailability') is not None else None,
last_reported_at=datetime.strptime(appliance['lastReportedAt'], _LAST_REPORTED_AT) if appliance[
'lastReportedAt'] else None,
model=str(appliance['model']) if appliance.get('model') is not None else None,
network_name=str(appliance['networkName']) if appliance.get('networkName') is not None else None,
serial=str(appliance['serial']) if appliance.get('serial') is not None else None,
uplinks={uplink['interface']: ApplianceUplink.parse(uplink) for uplink in appliance.get('uplinks', [])},
)
def parse_appliance_uplinks(string_table: StringTable) -> Appliance | None:
json_data = load_json(string_table)
json_data = json_data[0]
return Appliance(
serial=str(json_data['serial']) if json_data.get('serial') is not None else None,
model=str(json_data['model']) if json_data.get('model') is not None else None,
last_reported_at=datetime.strptime(json_data['lastReportedAt'], _LAST_REPORTED_AT) if json_data[
'lastReportedAt'] else None,
network_name=str(json_data['networkName']) if json_data.get('networkName') is not None else None,
high_availability=ApplianceUplinkHA(
enabled=bool(json_data['highAvailability']['enabled']) if json_data['highAvailability'].get(
'enabled') is not None else None,
role=str(json_data['highAvailability']['role']) if json_data['highAvailability'].get(
'role') is not None else None,
) if json_data.get('highAvailability') is not None else None,
uplinks={
uplink['interface']: ApplianceUplink(
interface=str(uplink['interface']) if uplink.get('interface') is not None else None,
gateway=str(uplink['gateway']) if uplink.get('gateway') is not None else None,
ip=str(uplink['ip']) if uplink.get('ip') is not None else None,
ip_assigned_by=str(uplink['ipAssignedBy']) if uplink.get('ipAssignedBy') is not None else None,
primary_dns=str(uplink['primaryDns']) if uplink.get('primaryDns') is not None else None,
public_ip=str(uplink['publicIp']) if uplink.get('publicIp') is not None else None,
secondary_dns=str(uplink['secondaryDns']) if uplink.get('secondaryDns') is not None else None,
status=str(uplink['status']) if uplink.get('status') is not None else None,
received=get_int(uplink.get('received')),
sent=get_int(uplink.get('sent')),
) for uplink in json_data.get('uplinks', [])
},
)
return Appliance.parse(json_data[0])
register.agent_section(
......@@ -153,11 +162,13 @@ def discover_appliance_uplinks(section: Appliance) -> DiscoveryResult:
_STATUS_MAP = {
"active": 0,
"ready": 0,
"not connected": 1,
"failed": 2,
"not connected": 1,
"ready": 0,
}
_TIMESPAN = 60
def check_appliance_uplinks(item: str, params: Mapping[str, any], section: Appliance) -> CheckResult:
try:
......@@ -169,29 +180,47 @@ def check_appliance_uplinks(item: str, params: Mapping[str, any], section: Appli
_STATUS_MAP.update(params['status_map'])
yield Result(state=State(_STATUS_MAP.get(uplink.status, 3)), summary=f'Status: {uplink.status}')
yield Result(state=State.OK, summary=f'IP: {uplink.ip}')
yield Result(state=State.OK, summary=f'Public IP: {uplink.public_ip}')
if uplink.ip:
yield Result(state=State.OK, summary=f'IP: {uplink.ip}')
if uplink.public_ip:
yield Result(state=State.OK, summary=f'Public IP: {uplink.public_ip}')
yield Result(state=State.OK, notice=f'Network: {section.network_name}')
if uplink.received:
yield Result(state=State.OK, summary=f'in: {render.networkbandwidth(uplink.received)}')
yield Metric(name='if_in_bps', value=uplink.received * 8)
if uplink.sent:
yield Result(state=State.OK, summary=f'Out: {render.networkbandwidth(uplink.sent)}')
yield Metric(name='if_out_bps', value=uplink.sent * 8)
# not needed, will show in device status
if uplink.received: # and params.get('show_traffic'):
value = uplink.received * 8 / _TIMESPAN # Bits / Timespan
yield from check_levels(
value=value, # Bits
label='In',
metric_name='if_in_bps',
render_func=lambda v: render.networkbandwidth(v/8), # Bytes
# notice_only=True,
)
if uplink.sent: # and params.get('show_traffic'):
value = uplink.sent * 8 / _TIMESPAN # Bits / Timespan
yield from check_levels(
value=value, # Bits
label='Out',
metric_name='if_out_bps',
render_func=lambda v: render.networkbandwidth(v/8), # Bytes
# notice_only=True,
)
# not needed, will show in device status (?)
# yield from check_last_reported_ts(last_reported_ts=section.last_reported_at.timestamp())
# not sure if this is usefully, need system with H/A enabled=True to check
yield Result(state=State.OK, notice=f'H/A enabled: {section.high_availability.enabled}')
yield Result(state=State.OK, notice=f'H/A role: {section.high_availability.role}')
yield Result(state=State.OK, notice=f'Gateway: {uplink.gateway}')
yield Result(state=State.OK, notice=f'IP assigned by: {uplink.ip_assigned_by}')
yield Result(state=State.OK, notice=f'Primary DNS: {uplink.primary_dns}')
yield Result(state=State.OK, notice=f'Secondary DNS: {uplink.secondary_dns}')
if uplink.gateway:
yield Result(state=State.OK, notice=f'Gateway: {uplink.gateway}')
if uplink.ip_assigned_by:
yield Result(state=State.OK, notice=f'IP assigned by: {uplink.ip_assigned_by}')
if uplink.primary_dns:
yield Result(state=State.OK, notice=f'Primary DNS: {uplink.primary_dns}')
if uplink.secondary_dns:
yield Result(state=State.OK, notice=f'Secondary DNS: {uplink.secondary_dns}')
register.check_plugin(
......
......@@ -9,9 +9,11 @@
# File : cisco_meraki_appliance_vpns.py (check plugin)
# 2024-04-27: made data parsing more robust
# 2024-05-15: moved parse function to data classes
from abc import abstractmethod
from dataclasses import dataclass
from _collections_abc import Sequence, Mapping
from _collections_abc import Mapping, Sequence
from cmk.base.plugins.agent_based.agent_based_api.v1 import (
register,
......@@ -28,23 +30,6 @@ from cmk.base.plugins.agent_based.utils.cisco_meraki import (
load_json,
)
@dataclass(frozen=True)
class ApplianceVPNUplink:
interface: str | None
public_ip: str | None
@dataclass(frozen=True)
class ApplianceVPNPeer:
type: str | None
network_name: str | None
reachability: str | None
public_ip: str | None
device_vpn_mode: str | None
uplinks: Sequence[ApplianceVPNUplink] | None
# sample string_table
__appliance_vpn_statuses = [
{
......@@ -105,35 +90,73 @@ __appliance_vpn_statuses = [
]
def parse_appliance_vpns(string_table: StringTable) -> Mapping[str, ApplianceVPNPeer]:
json_data = load_json(string_table)
json_data = json_data[0]
@dataclass(frozen=True)
class ApplianceVPNUplink:
interface: str | None
public_ip: str | None
vpn_uplinks = [ApplianceVPNUplink(
interface=str(uplink['interface']) if uplink.get('interface') is not None else None,
public_ip=str(uplink['publicIp']) if uplink.get('publicIp') is not None else None,
) for uplink in json_data.get('uplinks', [])]
@classmethod
def parse(cls, uplink: Mapping[str, object]):
return cls(
interface=str(uplink['interface']) if uplink.get('interface') is not None else None,
public_ip=str(uplink['publicIp']) if uplink.get('publicIp') is not None else None,
)
meraki_peers = {
peer['networkName']: ApplianceVPNPeer(
type='Meraki VPN Peer',
@dataclass(frozen=True)
class ApplianceVPNPeer:
device_vpn_mode: str | None
network_name: str | None
public_ip: str | None
reachability: str | None
type: str | None
uplinks: Sequence[ApplianceVPNUplink] | None
@abstractmethod
def parse(self, peer: Mapping[str, object], uplinks: Sequence[ApplianceVPNUplink], mode: str | None):
raise NotImplementedError()
@dataclass(frozen=True)
class ApplianceVPNPeerMeraki(ApplianceVPNPeer):
@classmethod
def parse(cls, peer: Mapping[str, object], uplinks: Sequence[ApplianceVPNUplink], mode: str | None):
return cls(
device_vpn_mode=str(mode) if mode is not None else None,
network_name=str(peer['networkName']) if peer.get('networkName') is not None else None,
reachability=str(peer['reachability']) if peer.get('reachability') is not None else None,
public_ip=None,
device_vpn_mode=str(json_data['vpnMode']) if peer.get('vpnMode') is not None else None,
uplinks=vpn_uplinks,
) for peer in json_data.get('merakiVpnPeers', [])
}
third_party_peers = {
peer['name']: ApplianceVPNPeer(
type='third party VPN Peer',
network_name=str(peer['name']) if peer.get('name') is not None else None,
reachability=str(peer['reachability']) if peer.get('reachability') is not None else None,
type='Meraki VPN Peer',
uplinks=uplinks,
)
@dataclass(frozen=True)
class ApplianceVPNPeerThirdParty(ApplianceVPNPeer):
@classmethod
def parse(cls, peer: Mapping[str, object], uplinks: Sequence[ApplianceVPNUplink], mode: str | None):
return cls(
device_vpn_mode=str(mode) if mode is not None else None,
network_name=str(peer['name']) if peer.get('name') is not None else None,
public_ip=str(peer['publicIp']) if peer.get('publicIp') is not None else None,
device_vpn_mode=str(json_data['vpnMode']) if peer.get('vpnMode') is not None else None,
uplinks=vpn_uplinks,
) for peer in json_data.get('thirdPartyVpnPeers', [])
}
reachability=str(peer['reachability']) if peer.get('reachability') is not None else None,
type='third party VPN Peer',
uplinks=uplinks,
)
def parse_appliance_vpns(string_table: StringTable) -> Mapping[str, ApplianceVPNPeer]:
json_data = load_json(string_table)
json_data = json_data[0]
vpn_uplinks = [ApplianceVPNUplink.parse(uplink) for uplink in json_data.get('uplinks', [])]
meraki_peers = {peer['networkName']: ApplianceVPNPeerMeraki.parse(
peer, vpn_uplinks, json_data.get('vpnMode')) for peer in json_data.get('merakiVpnPeers', [])}
third_party_peers = {
peer['name']: ApplianceVPNPeerThirdParty.parse(
peer, vpn_uplinks, json_data.get('vpnMode')) for peer in json_data.get('thirdPartyVpnPeers', [])}
meraki_peers.update(third_party_peers)
......
......@@ -11,16 +11,16 @@
# 2024-04-27: made data parsing more robust
from _collections_abc import Mapping
from dataclasses import dataclass
from datetime import datetime
from _collections_abc import Mapping
from cmk.base.plugins.agent_based.agent_based_api.v1 import (
register,
Metric,
Result,
Service,
State,
Metric,
register,
render,
)
from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import (
......@@ -29,8 +29,8 @@ from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import (
StringTable,
)
from cmk.base.plugins.agent_based.utils.cisco_meraki import (
get_int, # type: ignore[import]
load_json,
get_int,
)
......@@ -50,12 +50,12 @@ class CellularUplink:
model: str | None
provider: str | None
public_ip: str | None
signal_type: str | None
status: str | None
received: int | None
rsrp: int | None
rsrq: int | None
sent: int | None
received: int | None
signal_type: str | None
status: str | None
@dataclass(frozen=True)
......@@ -66,12 +66,12 @@ class CellularUplinkHA:
@dataclass(frozen=True)
class CellularGateway:
serial: str | None
model: str | None
high_availability: CellularUplinkHA | None
last_reported_at: datetime | None
model: str | None
# network_name: str
serial: str | None
uplinks: Mapping[str, CellularUplink] | None
high_availability: CellularUplinkHA | None
__cellular_uplinks = [
......@@ -186,7 +186,7 @@ def check_cellular_uplinks(item: str, params: Mapping[str, any], section: Cellul
yield Result(state=State.OK, summary=f'RSRQ: {uplink.rsrq} dB')
if uplink.received:
yield Result(state=State.OK, summary=f'in: {render.networkbandwidth(uplink.received)}')
yield Result(state=State.OK, summary=f'In: {render.networkbandwidth(uplink.received)}')
yield Metric(name='if_in_bps', value=uplink.received * 8)
if uplink.sent:
......
......@@ -68,7 +68,7 @@ class DeviceStatus:
return cls(
status=str(row["status"]),
last_reported=cls._parse_last_reported(str(row["lastReportedAt"])),
power_supplys=cls._parse_powesuplys(_components=row['components']) if row.get('components') else [],
power_supplys=cls._parse_powersupplies(_components=row['components']) if row.get('components') else [],
unknown_components=row.get('components') if row.get('components', {}) != {} else None,
)
......@@ -80,7 +80,7 @@ class DeviceStatus:
return None
@staticmethod
def _parse_powesuplys(_components: Mapping[str, any] | None) -> Sequence[PowerSupply] | None:
def _parse_powersupplies(_components: Mapping[str, any] | None) -> Sequence[PowerSupply] | None:
def _parse_poe(_poe: Mapping[str, any]) -> Poe | None:
try:
return Poe(
......
......@@ -13,17 +13,17 @@ from collections.abc import Sequence
from typing import Final
from cmk.base.plugins.agent_based.agent_based_api.v1 import (
register,
TableRow,
register,
)
from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import (
InventoryResult,
StringTable,
)
from cmk.base.plugins.agent_based.utils.cisco_meraki import (
load_json,
MerakiAPIData,
MerakiNetwork,
MerakiNetwork, # type: ignore[import]
load_json,
)
_API_NAME_ORGANISATION_NAME: Final = "name"
......@@ -37,6 +37,18 @@ _is_bounf_to_template = {
@dataclass(frozen=True)
class NetworkInfo(MerakiNetwork):
enrollment_string: str | None
id: str
is_bound_to_config_template: bool
name: str
notes: str
organisation_id: str
organisation_name: str
product_types: Sequence[str]
tags: Sequence[str]
time_zone: str
url: str
@classmethod
def parse(cls, organisations: MerakiAPIData) -> "NetworkInfo":
networks = []
......@@ -44,16 +56,16 @@ class NetworkInfo(MerakiNetwork):
for network in organisation:
networks += [network]
return [cls(
enrollment_string=network.get('enrollmentString', None),
id=network['id'],
is_bound_to_config_template=network['isBoundToConfigTemplate'],
name=network['name'],
product_types=network['productTypes'],
time_zone=network['timeZone'],
tags=network['tags'],
enrollment_string=network.get('enrollmentString', None),
notes=network['notes'],
is_bound_to_config_template=network['isBoundToConfigTemplate'],
organisation_id=network['organizationId'],
organisation_name=network['organizationName'],
product_types=network['productTypes'],
tags=network['tags'],
time_zone=network['timeZone'],
url=network['url'],
) for network in networks]
......
......@@ -10,15 +10,15 @@
# 2024-04-27: made data parsing more robust
from _collections_abc import Mapping
from dataclasses import dataclass
from _collections_abc import Mapping, Sequence
from cmk.base.plugins.agent_based.agent_based_api.v1 import (
register,
Metric,
Result,
Service,
State,
Metric,
register,
)
from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import (
CheckResult,
......@@ -26,22 +26,22 @@ from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import (
StringTable,
)
from cmk.base.plugins.agent_based.utils.cisco_meraki import (
get_int, # type: ignore[import]
load_json,
get_int,
)
@dataclass(frozen=True)
class SSDI:
name: str | None
number: int | None
band: str | None
channel: int | None
channel_width: str | None
power: str | None
broadcasting: bool | None
bssid: str | None
channel: int | None
channel_width: str | None
enabled: bool | None
name: str | None
number: int | None
power: str | None
visible: bool | None
......@@ -60,15 +60,15 @@ def parse_wireless_device_status(string_table: StringTable):
item = str(ssid_number) + ' on band ' + row.get('band')
ssids[item] = SSDI(
name=str(row['ssidName']) if row.get('ssidName') is not None else None,
number=get_int(row.get('ssidNumber')),
band=str(row['band']) if row.get('band') is not None else None,
channel=get_int(row.get('channel')),
channel_width=str(row['channelWidth']) if row.get('channelWidth') is not None else None,
power=str(row['power']) if row.get('power') is not None else None,
broadcasting=bool(row['broadcasting']) if row.get('broadcasting') is not None else None,
bssid=str(row['bssid']) if row.get('') is not None else None,
channel=get_int(row.get('channel')),
channel_width=str(row['channelWidth']) if row.get('channelWidth') is not None else None,
enabled=bool(row['enabled']) if row.get('enabled') is not None else None,
name=str(row['ssidName']) if row.get('ssidName') is not None else None,
number=get_int(row.get('ssidNumber')),
power=str(row['power']) if row.get('power') is not None else None,
visible=bool(row['enabled']) if row.get('enabled') is not None else None,
)
......
......@@ -11,13 +11,13 @@
# 2024-04-27: made data parsing more robust
from dataclasses import dataclass
from _collections_abc import Mapping, Sequence
from _collections_abc import Mapping
from cmk.base.plugins.agent_based.agent_based_api.v1 import (
register,
Result,
Service,
State,
register,
render,
)
from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import (
......@@ -26,8 +26,8 @@ from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import (
StringTable,
)
from cmk.base.plugins.agent_based.utils.cisco_meraki import (
get_int, # type: ignore[import]
load_json,
get_int,
)
__ethernet_port_statuses = {
......
......@@ -13,9 +13,11 @@
# 2024-05-12: added support for MerakiGetOrganizationSwitchPortsStatusesBySwitch (Early Access)
# added support for "realtime" traffic counters
# refactoring parse functions as class method
# 2024-05-20: added discovery rule for port status
from dataclasses import dataclass
from _collections_abc import Mapping, Sequence
from dataclasses import dataclass
from cmk.base.plugins.agent_based.agent_based_api.v1 import (
Result,
......@@ -26,6 +28,7 @@ from cmk.base.plugins.agent_based.agent_based_api.v1 import (
register,
render,
)
from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import (
CheckResult,
DiscoveryResult,
......@@ -103,14 +106,14 @@ class SwitchPortLLDP:
@dataclass(frozen=True)
class SwitchPortUsage:
total: int
sent: int
recv: int
total: float
sent: float
recv: float
@classmethod
def parse(cls, usage: Mapping[str, int] | None):
def parse(cls, usage: Mapping[str, float] | None):
"""
Usage in Kbp.
Usage in KiloBits -> changed to Bits.
Args:
usage: Mapping with the keys 'total', 'sent', 'recv', the values are all int.
I.e. {"total": 1944476, "sent": 1099104, "recv": 845372}
......@@ -119,36 +122,58 @@ class SwitchPortUsage:
"""
return cls(
total=get_int(usage.get('total')),
sent=get_int(usage.get('sent')),
recv=get_int(usage.get('recv')),
total=get_float(usage.get('total')) * 1000,
sent=get_float(usage.get('sent')) * 1000,
recv=get_float(usage.get('recv')) * 1000,
) if usage else None
@dataclass(frozen=True)
class SwitchPortTraffic:
total: int
sent: int
recv: int
total: float # The average speed of the data sent and received (in kilobits-per-second).
sent: float # The average speed of the data sent (in kilobits-per-second).
recv: float # The average speed of the data received (in kilobits-per-second).
@classmethod
def parse(cls, traffic: Mapping[str, int] | None):
def parse(cls, traffic: Mapping[str, float] | None):
"""
Traffic values are in Kbp/s.
A breakdown of the average speed of data that has passed through this port during the timespan.
input traffic values are KiloBts/s. Output values are changed to Bits/s
Args:
traffic: Mapping with the keys 'total', 'sent', 'recv', the values are all int.
traffic: Mapping with the keys 'total', 'sent', 'recv', the values are all float.
I.e. {"total": 184.4, "sent": 104.2, "recv": 80.2}
Returns:
"""
return cls(
total=get_int(traffic.get('total')) * 1000,
sent=get_int(traffic.get('sent')) * 1000,
recv=get_int(traffic.get('recv')) * 1000,
total=get_float(traffic.get('total')) * 1000,
sent=get_float(traffic.get('sent')) * 1000,
recv=get_float(traffic.get('recv')) * 1000,
) if traffic else None
@dataclass(frozen=True)
class SwitchPortSpanningTree:
status: Sequence[str]
@classmethod
def parse(cls, spanning_tree: Mapping[str, Sequence[str]]):
"""
{"statuses": ["Forwarding", "Is edge", "Is peer-to-peer"]}
{"statuses": []}
Args:
spanning_tree:
Returns:
"""
if isinstance(spanning_tree, dict):
return cls(
status=[str(status) for status in spanning_tree.get('statuses', [])]
)
@dataclass(frozen=True)
class SwitchPort:
cdp: SwitchPortCDP | None
......@@ -166,6 +191,7 @@ class SwitchPort:
traffic: SwitchPortTraffic | None
usage: SwitchPortUsage | None
warnings: Sequence[str]
spanning_tree: SwitchPortSpanningTree | None
@classmethod
def parse(cls, port: Mapping[str, object]):
......@@ -184,7 +210,8 @@ class SwitchPort:
cdp=SwitchPortCDP.parse(port.get('cdp')),
secure_port=SwitchSecurePort.parse(port.get('securePort')),
usage=SwitchPortUsage.parse(port.get('usageInKb')),
traffic=SwitchPortTraffic.parse((port.get('trafficInKbps')))
traffic=SwitchPortTraffic.parse((port.get('trafficInKbps'))),
spanning_tree=SwitchPortSpanningTree.parse(port.get('spanningTree'))
)
......@@ -215,16 +242,18 @@ register.agent_section(
)
def discover_switch_ports_statuses(section: Mapping[str, SwitchPort]) -> DiscoveryResult:
def discover_switch_ports_statuses(params: Mapping[str, object], section: Mapping[str, SwitchPort]) -> DiscoveryResult:
discovered_port_states = params['discovered_port_states']
for port in section.keys():
yield Service(
item=port,
parameters={
'enabled': section[port].enabled,
'status': section[port].status,
'speed': section[port].speed,
}
)
if section[port].enabled in discovered_port_states and section[port].status in discovered_port_states:
yield Service(
item=port,
parameters={
'enabled': section[port].enabled,
'status': section[port].status,
'speed': section[port].speed,
}
)
def check_switch_ports_statuses(item: str, params: Mapping[str, any], section: Mapping[str, SwitchPort]) -> CheckResult:
......@@ -274,26 +303,18 @@ def check_switch_ports_statuses(item: str, params: Mapping[str, any], section: M
yield Result(state=State.OK, summary=f'Clients: {port.client_count}')
if params.get('show_traffic'):
# add perf data from usage and traffic
# yield from check_levels(
# value=port.traffic.total,
# label='Traffic total',
# metric_name='traffic_total',
# render_func=lambda v: render.networkbandwidth(v / 8), # needs to changed to bytes ber second
# notice_only=True,
# )
yield from check_levels(
value=port.traffic.sent,
value=port.traffic.sent, # Bits
label='Out',
metric_name='traffic_sent',
render_func=lambda v: render.networkbandwidth(v / 8), # needs to changed to bytes ber second
metric_name='if_out_bps',
render_func=lambda v: render.networkbandwidth(v/8), # Bytes
# notice_only=True,
)
yield from check_levels(
value=port.traffic.recv,
value=port.traffic.recv, # Bits
label='In',
metric_name='traffic_received',
render_func=lambda v: render.networkbandwidth(v / 8), # needs to changed to bytes ber second
metric_name='if_in_bps',
render_func=lambda v: render.networkbandwidth(v/8), # Bytes
# notice_only=True,
)
else:
......@@ -309,6 +330,10 @@ def check_switch_ports_statuses(item: str, params: Mapping[str, any], section: M
if port.power_usage_in_wh:
yield Result(state=State.OK, summary=f'Power usage: {port.power_usage_in_wh} Wh')
if port.spanning_tree:
for status in port.spanning_tree.status:
yield Result(state=State.OK, notice=f'Spanning-tree status: {status}')
for warning in port.warnings:
yield Result(state=State.WARN, notice=f'{warning}')
for error in port.errors:
......@@ -333,6 +358,10 @@ register.check_plugin(
'state_op_change': 1,
},
check_ruleset_name='cisco_meraki_switch_ports_statuses',
discovery_ruleset_name='discovery_cisco_meraki_switch_ports_statuses',
discovery_default_parameters={
'discovered_port_states': [True, False, 'Connected', 'Disconnected']
}
)
......
......@@ -26,6 +26,7 @@ _SEC_NAME_NETWORKS: Final = "_networks" # internal use, runs always, needed for
_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_CELLULAR_UPLINKS: Final = "cellular-uplinks"
_SEC_NAME_DEVICE_STATUSES: Final = "device-statuses"
......
......@@ -152,56 +152,56 @@ perfometer_info.append({
#
# switch port statuses
#
check_metrics['check_mk-cisco_meraki_organisations_api'] = {
'traffic_total': {'auto_graph': False},
'traffic_sent': {'auto_graph': False},
'traffic_received': {'auto_graph': False},
}
metric_info["traffic_total"] = {
"title": _("Total bandwidth"),
"unit": "bits/s",
"color": "11/a",
}
metric_info["traffic_sent"] = {
"title": _("Output bandwidth"),
"unit": "bits/s",
"color": "#0080e0",
}
metric_info["traffic_received"] = {
"title": _("Input bandwidth"),
"unit": "bits/s",
"color": "#00e060",
}
graph_info['cisco_meraki.switch_port_status.traffic'] = {
'title': _('Traffic'),
'metrics': [
('traffic_received', 'area'),
('traffic_sent', '-area'),
],
}
perfometer_info.append(
{
"type": "dual",
"perfometers": [
{
"type": "logarithmic",
"metric": "traffic_received",
"half_value": 500000,
"exponent": 5,
},
{
"type": "logarithmic",
"metric": "traffic_sent",
"half_value": 500000,
"exponent": 5,
},
],
}
)
# check_metrics['check_mk-cisco_meraki_organisations_api'] = {
# 'traffic_total': {'auto_graph': False},
# 'traffic_sent': {'auto_graph': False},
# 'traffic_received': {'auto_graph': False},
# }
#
# metric_info["traffic_total"] = {
# "title": _("Total bandwidth"),
# "unit": "bits/s",
# "color": "11/a",
# }
#
# metric_info["traffic_sent"] = {
# "title": _("Output bandwidth"),
# "unit": "bits/s",
# "color": "#0080e0",
# }
#
# metric_info["traffic_received"] = {
# "title": _("Input bandwidth"),
# "unit": "bits/s",
# "color": "#00e060",
# }
#
# graph_info['cisco_meraki.switch_port_status.traffic'] = {
# 'title': _('Traffic'),
# 'metrics': [
# ('traffic_received', 'area'),
# ('traffic_sent', '-area'),
# ],
# }
# perfometer_info.append(
# {
# "type": "dual",
# "perfometers": [
# {
# "type": "logarithmic",
# "metric": "traffic_received",
# "half_value": 500000,
# "exponent": 5,
# },
# {
# "type": "logarithmic",
# "metric": "traffic_sent",
# "half_value": 500000,
# "exponent": 5,
# },
# ],
# }
# )
#
# API return Codes
......@@ -281,3 +281,28 @@ perfometer_info.append({
"half_value": 100,
"exponent": 5,
})
# testing only
# metric_info["usage_out"] = {
# "title": _("Usage Out"),
# "unit": "count",
# "color": "#0080e0",
# }
#
# metric_info["usage_in"] = {
# "title": _("Usage In"),
# "unit": "count",
# "color": "#00e060",
# }
#
# graph_info['cisco_meraki.switch_port_status.usage'] = {
# 'title': _('Usage'),
# 'metrics': [
# ('usage_in', 'area'),
# ('usage_out', '-area'),
# ],
# 'optional_metrics': [
# 'usage_in',
# 'usage_out',
# ]
# }
\ No newline at end of file
......@@ -11,13 +11,14 @@
from cmk.gui.i18n import _
from cmk.gui.plugins.wato.utils import (
CheckParameterRulespecWithItem,
rulespec_registry,
RulespecGroupCheckParametersNetworking,
rulespec_registry,
)
from cmk.gui.valuespec import (
Dictionary,
TextInput,
FixedValue,
MonitoringState,
TextInput,
)
......@@ -51,7 +52,18 @@ def _parameter_valuespec_cisco_meraki_org_appliance_uplinks():
default_value=2,
)),
]
))
)),
# not needed, if we don't want usage -> disable in agent
# ('show_traffic',
# FixedValue(
# True,
# title=_('Show bandwidth (use only with cache disabled)'),
# totext='Bandwidth monitoring enabled',
# help=_(
# 'Use only with cache disabled in the Meraki special agent settings. '
# 'The throughput be based on the usage for the last 60 seconds.'
# )
# ))
],
)
......
......@@ -84,9 +84,7 @@ rulespec_registry.register(
check_group_name="cisco_meraki_org_licenses_overview",
group=RulespecGroupCheckParametersApplications,
parameter_valuespec=_parameter_valuespec_cisco_meraki_org_licenses_overview,
item_spec=lambda: TextInput(
title=_("The organisation"),
),
item_spec=lambda: TextInput(title=_("The organisation"), ),
match_type="dict",
)
)
......@@ -13,10 +13,10 @@
from cmk.gui.i18n import _
from cmk.gui.plugins.wato.utils import (
rulespec_registry,
DropdownChoice,
HostRulespec,
RulespecGroupCheckParametersDiscovery,
DropdownChoice,
rulespec_registry,
)
from cmk.gui.valuespec import (
Dictionary,
......
......@@ -46,9 +46,7 @@ rulespec_registry.register(
check_group_name="cisco_meraki_organisations_api",
group=RulespecGroupCheckParametersApplications,
parameter_valuespec=_parameter_valuespec_cisco_meraki_organisations_api,
item_spec=lambda: TextInput(
title=_("The organisation"),
),
item_spec=lambda: TextInput(title=_("The organisation"), ),
match_type="dict",
)
)
......@@ -10,15 +10,20 @@
# 2024-05-12: added support for MerakiGetOrganizationSwitchPortsStatusesBySwitch (Early Access)
# added traffic counters as perfdata
# 2024-05-19: reworked switch port traffic
# 2024-05-20: added discovery rule for port status
from cmk.gui.i18n import _
from cmk.gui.plugins.wato.utils import (
CheckParameterRulespecWithItem,
rulespec_registry,
HostRulespec,
RulespecGroupCheckParametersDiscovery,
RulespecGroupCheckParametersNetworking,
rulespec_registry,
)
from cmk.gui.valuespec import (
Dictionary,
ListChoice,
FixedValue,
MonitoringState,
TextInput,
......@@ -94,3 +99,34 @@ rulespec_registry.register(
item_spec=lambda: TextInput(title=_('The Port ID'), ),
)
)
def _valuespec_discovery_cisco_meraki_switch_ports_statuses():
return Dictionary(
title=_('Cisco Meraki Switch Ports'),
elements=[
('discovered_port_states',
ListChoice(
title=_('Select Ports to discover'),
choices=[
(True, _('Admin enabled')),
(False, _('Admin disabled')),
('Connected', _('Connected')),
('Disconnected', _('Disconnected')),
],
help=_('Select the port states for discovery'),
default_value=[True, False, 'Connected', 'Disconnected'],
)),
],
required_keys=['item_variant'],
)
rulespec_registry.register(
HostRulespec(
group=RulespecGroupCheckParametersDiscovery,
match_type='dict',
name='discovery_cisco_meraki_switch_ports_statuses',
valuespec=_valuespec_discovery_cisco_meraki_switch_ports_statuses,
))
\ No newline at end of file
......@@ -57,7 +57,7 @@
'plugins/wato/agent_cisco_meraki.py']},
'name': 'cisco_meraki',
'title': 'Cisco Meraki special agent',
'version': '1.2.12-20240512',
'version': '1.2.14-20240521',
'version.min_required': '2.2.0b1',
'version.packaged': '2.2.0p24',
'version.usable_until': '2.3.0b1'}
......@@ -21,6 +21,7 @@
# 2023-11-22: replaced host_suffix_prefix option by org_id_as_prefix
# changed excluded_sections option from DualListChoice to ListChoice to avoid the "Selected" header
# in conjunction with "excluded Sections" title
# 2024-05-15: added api_key to required_keys
from _collections_abc import Sequence
from cmk.gui.i18n import _
......@@ -43,18 +44,20 @@ from cmk.base.plugins.agent_based.utils.cisco_meraki import (
# _SEC_NAME_DEVICE_INFO,
# _SEC_NAME_NETWORKS,
# _SEC_NAME_ORGANISATIONS,
_SEC_NAME_LICENSES_OVERVIEW,
_SEC_NAME_DEVICE_STATUSES,
_SEC_NAME_SENSOR_READINGS,
_SEC_NAME_DEVICE_UPLINKS_INFO,
_SEC_NAME_APPLIANCE_UPLINKS,
_SEC_NAME_APPLIANCE_VPNS,
_SEC_NAME_SWITCH_PORTS_STATUSES,
_SEC_NAME_WIRELESS_ETHERNET_STATUSES,
_SEC_NAME_WIRELESS_DEVICE_STATUS,
_SEC_NAME_CELLULAR_UPLINKS,
_SEC_NAME_ORG_API_REQUESTS, # type: ignore[import]
_SEC_NAME_LICENSES_OVERVIEW, # type: ignore[import]
_SEC_NAME_DEVICE_STATUSES, # type: ignore[import]
_SEC_NAME_SENSOR_READINGS, # type: ignore[import]
_SEC_NAME_DEVICE_UPLINKS_INFO, # type: ignore[import]
_SEC_NAME_APPLIANCE_UPLINKS, # type: ignore[import]
_SEC_NAME_APPLIANCE_UPLINKS_USAGE, # type: ignore[import]
_SEC_NAME_APPLIANCE_VPNS, # type: ignore[import]
_SEC_NAME_SWITCH_PORTS_STATUSES, # type: ignore[import]
_SEC_NAME_WIRELESS_ETHERNET_STATUSES, # type: ignore[import]
_SEC_NAME_WIRELESS_DEVICE_STATUS, # type: ignore[import]
_SEC_NAME_CELLULAR_UPLINKS, # type: ignore[import]
# Early Access
_SEC_NAME_ORG_SWITCH_PORTS_STATUSES,
_SEC_NAME_ORG_SWITCH_PORTS_STATUSES, # type: ignore[import]
)
......@@ -71,26 +74,26 @@ def _validate_orgs(value: Sequence[str] | None, var_prefix: str):
def _valuespec_special_agent_cisco_meraki() -> ValueSpec:
return Dictionary(
title=_("Cisco Meraki"),
title=_('Cisco Meraki'),
elements=[
("api_key", IndividualOrStoredPassword(
title=_("API Key"),
('api_key', IndividualOrStoredPassword(
title=_('API Key'),
allow_empty=False,
help=_('The key to access the Cisco Meraki Cloud Rest API.')
)),
("proxy", HTTPProxyReference(),),
("no_cache", FixedValue(
('proxy', HTTPProxyReference(),),
('no_cache', FixedValue(
value=True,
title=_("Disable Cache"),
title=_('Disable Cache'),
totext=_(''),
help=_(
'Never use cached information. By default the agent will cache received '
'data to avoid API limits and speed up the data retrievel.'
)
)),
("org_id_as_prefix", FixedValue(
('org_id_as_prefix', FixedValue(
value=True,
title=_("Uese organisation ID as host prefix"),
title=_('Uese organisation ID as host prefix'),
totext=_(''),
help=_(
'The organisation ID will be used as prefix for the hostname (separated by a "\'"). Use '
......@@ -100,17 +103,19 @@ def _valuespec_special_agent_cisco_meraki() -> ValueSpec:
'folders.'
)
)),
("excluded_sections",
('excluded_sections',
ListChoice(
title=_("excluded Sections"),
title=_('excluded Sections'),
choices=[
(_SEC_NAME_APPLIANCE_UPLINKS, _("Appliances uplinks")),
(_SEC_NAME_APPLIANCE_VPNS, _("Appliances VPNs")),
(_SEC_NAME_ORG_API_REQUESTS, _('API request')),
(_SEC_NAME_APPLIANCE_UPLINKS, _('Appliances uplinks')),
(_SEC_NAME_APPLIANCE_UPLINKS_USAGE, _('Appliances uplinks usage')),
(_SEC_NAME_APPLIANCE_VPNS, _('Appliances VPNs')),
(_SEC_NAME_CELLULAR_UPLINKS, _('Cellular devices uplinks')),
(_SEC_NAME_DEVICE_STATUSES, _("Devices status")),
(_SEC_NAME_DEVICE_UPLINKS_INFO, _("Devices uplink info")),
(_SEC_NAME_LICENSES_OVERVIEW, _("Licenses overview")),
(_SEC_NAME_SENSOR_READINGS, _("Sensors readings")),
(_SEC_NAME_DEVICE_STATUSES, _('Devices status')),
(_SEC_NAME_DEVICE_UPLINKS_INFO, _('Devices uplink info')),
(_SEC_NAME_LICENSES_OVERVIEW, _('Licenses overview')),
(_SEC_NAME_SENSOR_READINGS, _('Sensors readings')),
(_SEC_NAME_SWITCH_PORTS_STATUSES, _('Switch ports status')),
(_SEC_NAME_WIRELESS_ETHERNET_STATUSES, _('Wireless devices ethernet status')),
(_SEC_NAME_WIRELESS_DEVICE_STATUS, _('Wireless devices SSIDs status')),
......@@ -119,9 +124,9 @@ def _valuespec_special_agent_cisco_meraki() -> ValueSpec:
help=_('Query only the selected sections. Default is Query all sections.'),
default_value=[_SEC_NAME_ORG_SWITCH_PORTS_STATUSES],
)),
("orgs",
('orgs',
ListOfStrings(
title=_("Organisation IDs"),
title=_('Organisation IDs'),
help=_('List of Organisation IDs to query. Defaulr is all Organisation IDs'),
allow_empty=False,
validate=_validate_orgs,
......@@ -129,14 +134,14 @@ def _valuespec_special_agent_cisco_meraki() -> ValueSpec:
],
optional_keys=True,
ignored_keys=['sections', 'host_suffix_prefix'],
required_keys=['excluded_sections'],
required_keys=['excluded_sections', 'api_key'],
)
rulespec_registry.register(
HostRulespec(
group=RulespecGroupDatasourceProgramsApps,
name="special_agents:cisco_meraki",
name='special_agents:cisco_meraki',
valuespec=_valuespec_special_agent_cisco_meraki,
)
)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment