From aeb423bf0b684edb94bf70f9f7f08ae44be25c62 Mon Sep 17 00:00:00 2001 From: Thorsten Spille Date: Wed, 23 Jun 2021 13:07:40 +0200 Subject: [PATCH] Initial Version 0.75 --- .../plugins/agent_based/unifi_controller.py | 532 ++++++++++++++++++ .../agents/special/agent_unifi_controller | 480 ++++++++++++++++ share/check_mk/checks/agent_unifi_controller | 26 + share/check_mk/inventory/unifi_controller | 76 +++ .../web/plugins/metrics/unifi_metrics.py | 209 +++++++ .../plugins/perfometer/unifi_performeter.py | 15 + .../wato/datasource_unifi_controller.py | 52 ++ 7 files changed, 1390 insertions(+) create mode 100644 lib/check_mk/base/plugins/agent_based/unifi_controller.py create mode 100644 share/check_mk/agents/special/agent_unifi_controller create mode 100644 share/check_mk/checks/agent_unifi_controller create mode 100644 share/check_mk/inventory/unifi_controller create mode 100644 share/check_mk/web/plugins/metrics/unifi_metrics.py create mode 100644 share/check_mk/web/plugins/perfometer/unifi_performeter.py create mode 100644 share/check_mk/web/plugins/wato/datasource_unifi_controller.py diff --git a/lib/check_mk/base/plugins/agent_based/unifi_controller.py b/lib/check_mk/base/plugins/agent_based/unifi_controller.py new file mode 100644 index 0000000..3b5c2b1 --- /dev/null +++ b/lib/check_mk/base/plugins/agent_based/unifi_controller.py @@ -0,0 +1,532 @@ +#!/usr/bin/env python3 +# -*- encoding: utf-8; py-indent-offset: 4 -*- +# +from cmk.gui.i18n import _ + +from .agent_based_api.v1 import ( + Metric, + register, + render, + Result, + Service, + State, +) +from .agent_based_api.v1.type_defs import CheckResult, DiscoveryResult +from typing import Any, Dict, Mapping, Sequence, Optional + +from dataclasses import dataclass +from collections import defaultdict +from .utils import interfaces + +SubSection = Dict[str,str] +Section = Dict[str, SubSection] +class dictobject(defaultdict): + def __getattr__(self,name): + return self[name] if name in self else "" + +nested_dictobject = lambda: dictobject(nested_dictobject) + +def _expect_bool(val,expected=True,failstate=State.WARN): + return State.OK if bool(int(val)) == expected else failstate + +def _expect_number(val,expected=0,failstate=State.WARN): + return State.OK if int(val) == expected else failstate + +def _safe_float(val): + try: + return float(val) + except (TypeError,ValueError): + return 0 + +def _unifi_status2state(status): + return { + "ok" : State.OK, + "warning" : State.WARN, + "error" : State.CRIT + }.get(status.lower(),State.UNKNOWN) + +from pprint import pprint + +def parse_unifi_dict(string_table): + _ret = dictobject() + for _line in string_table: + _ret[_line[0]] = _line[1] + return _ret + +def parse_unifi_nested_dict(string_table): + _ret = nested_dictobject() + for _line in string_table: + _ret[_line[0]][_line[1]] = _line[2] + return _ret + +############ Controller ############ +def discovery_unifi_controller(section): + yield Service(item="Unifi Controller") + if section.cloudkey_version: + yield Service(item="Cloudkey") + +def check_unifi_controller(item,section): + if item == "Unifi Controller": + yield Result( + state=State.OK, + summary=f"Version: {section.controller_version}" + ) + if int(section.update_available) > 0: + yield Result( + state=State.WARN, + notice=_("Update available") + ) + yield Metric("uptime",int(section.uptime)) + if item == "Cloudkey": + yield Result( + state=State.OK, + summary=f"Version: {section.cloudkey_version}" + ) + if int(section.cloudkey_update_available) > 0: + yield Result( + state=State.WARN, + notice=_("Update available") + ) + +register.agent_section( + name = 'unifi_controller', + parse_function = parse_unifi_dict +) + +register.check_plugin( + name='unifi_controller', + service_name='%s', + discovery_function=discovery_unifi_controller, + check_function=check_unifi_controller, +) +############ SITES ########### + +def discovery_unifi_sites(section): + for _item in section.values(): + yield Service(item=f"{_item.desc}") + +def check_unifi_sites(item,section): + site = next(filter(lambda x: x.desc == item,section.values())) + yield Metric("satisfaction",max(0,interfaces.saveint(site.satisfaction))) + + if site.lan_status != "unknown": + yield Metric("lan_user_sta",interfaces.saveint(site.lan_num_user)) + yield Metric("lan_guest_sta",interfaces.saveint(site.lan_num_guest)) + yield Metric("if_in_octets",interfaces.saveint(site.lan_rx_bytes_r)) + #yield Metric("if_in_bps",interfaces.saveint(site.lan_rx_bytes_r)*8) + yield Metric("if_out_octets",interfaces.saveint(site.lan_tx_bytes_r)) + #yield Metric("if_out_bps",interfaces.saveint(site.lan_tx_bytes_r)*8) + + yield Result( + state=_unifi_status2state(site.lan_status), + summary=f"LAN: {site.lan_num_sw}/{site.lan_num_adopted} Switch ({site.lan_status})" + ) + #yield Result( + # state=_expect_number(site.lan_num_disconnected), + # notice=f"{site.lan_num_disconnected} Switch disconnected" ##disconnected kann + #) + + if site.wlan_status != "unknown": + yield Metric("wlan_user_sta",interfaces.saveint(site.wlan_num_user)) + yield Metric("wlan_guest_sta",interfaces.saveint(site.wlan_num_guest)) + yield Metric("wlan_iot_sta",interfaces.saveint(site.wlan_num_iot)) + yield Metric("wlan_if_in_octets",interfaces.saveint(site.wlan_rx_bytes_r)) + yield Metric("wlan_if_out_octets",interfaces.saveint(site.wlan_tx_bytes_r)) + yield Result( + state=_unifi_status2state(site.wlan_status), + summary=f"WLAN: {site.wlan_num_ap}/{site.wlan_num_adopted} AP ({site.wlan_status})" + ) + #yield Result( + # state=_expect_number(site.wlan_num_disconnected), + # notice=f"{site.wlan_num_disconnected} AP disconnected" + #) + if site.wan_status != "unknown": + yield Result( + state=_unifi_status2state(site.wan_status), + summary=f"WAN Status: {site.wan_status}" + ) + if site.www_status != "unknown": + yield Result( + state=_unifi_status2state(site.www_status), + summary=f"WWW Status: {site.www_status}" + ) + if site.vpn_status != "unknown": + yield Result( + state=_unifi_status2state(site.vpn_status), + notice=f"WWW Status: {site.vpn_status}" + ) + yield Result( + state=_expect_number(site.num_new_alarms), + notice=f"{site.num_new_alarms} new Alarm" + ) + + +register.agent_section( + name = 'unifi_sites', + parse_function = parse_unifi_nested_dict +) + +register.check_plugin( + name='unifi_sites', + service_name='Site %s', + discovery_function=discovery_unifi_sites, + check_function=check_unifi_sites, +) + +############ DEVICE ########### +def discovery_unifi_device(section): + yield Service(item="Unifi Device") + yield Service(item="Uptime") + yield Service(item="Active-User") + if section.type != "uap": # kein satisfaction bei ap .. radio/ssid haben schon + yield Service(item="Satisfaction") + if section.general_temperature: + yield Service(item="Temperature") + if section.uplink_device: + yield Service(item="Uplink") + if section.speedtest_status: + yield Service(item="Speedtest") + +def check_unifi_device(item,section): + if item == "Unifi Device": + yield Result( + state=State.OK, + summary=f"Version: {section.version}" + ) + if interfaces.saveint(section.upgradable) > 0: + yield Result( + state=State.WARN, + notice=_("Update available") + ) + if item == "Active-User": + _active_user = interfaces.saveint(section.user_num_sta) + yield Result( + state=State.OK, + summary=f"{_active_user}" + ) + if interfaces.saveint(section.guest_num_sta) > -1: + yield Result( + state=State.OK, + summary=f"Guest: {section.guest_num_sta}" + ) + yield Metric("user_sta",_active_user) + yield Metric("guest_sta",interfaces.saveint(section.guest_num_sta)) + if item == "Uptime": + _uptime = int(section.uptime) if section.uptime else -1 + if _uptime > 0: + yield Result( + state=State.OK, + summary=render.timespan(_uptime) + ) + yield Metric("unifi_uptime",_uptime) + if item == "Satisfaction": + yield Result( + state=State.OK, + summary=f"{section.satisfaction}%" + ) + yield Metric("satisfaction",max(0,interfaces.saveint(section.satisfaction))) + if item == "Temperature": + yield Metric("temp",_safe_float(section.general_temperature)) + yield Result( + state=State.OK, + summary=f"{section.general_temperature} °C" + ) + if section.fan_level: + yield Result( + state=State.OK, + summary=f"Fan: {section.fan_level}%" + ) + if item == "Speedtest": + yield Result( + state=State.OK, + summary=f"Ping: {section.speedtest_ping} ms" + ) + yield Result( + state=State.OK, + summary=f"Down: {section.speedtest_download} Mbit/s" + ) + yield Result( + state=State.OK, + summary=f"Up: {section.speedtest_upload} Mbit/s" + ) + _speedtest_time = render.datetime(_safe_float(section.speedtest_time)) + yield Result( + state=State.OK, + summary=f"Last: {_speedtest_time}" + ) + yield Metric("rtt",_safe_float(section.speedtest_ping)) + yield Metric("if_in_bps",_safe_float(section.speedtest_download)*1024*1024) ## mbit to bit + yield Metric("if_out_bps",_safe_float(section.speedtest_upload)*1024*1024) ## mbit to bit + + if item == "Uplink": + yield Result( + state=_expect_bool(section.uplink_up), + summary=f"Device {section.uplink_device} Port: {section.uplink_remote_port}" + ) + +register.agent_section( + name = 'unifi_device', + parse_function = parse_unifi_dict +) + +register.check_plugin( + name='unifi_device', + service_name='%s', + discovery_function=discovery_unifi_device, + check_function=check_unifi_device, +) +############ DEVICEPORT ########### +@dataclass +class unifi_interface(interfaces.Interface): + jumbo : bool = False + satisfaction : int = 0 + poe_enable : bool = False + poe_mode : Optional[str] = None + poe_good : Optional[bool] = None + poe_current : Optional[float] = 0 + poe_power : Optional[float] = 0 + poe_voltage : Optional[float] = 0 + poe_class : Optional[str] = None + dot1x_mode : Optional[str] = None + dot1x_status : Optional[str] = None + ip_address : Optional[str] = None + + def __post_init__(self) -> None: + self.finalize() + +def _convert_unifi_counters_if(section: Section) -> interfaces.Section: + return [ + unifi_interface( + index=str(netif.port_idx), + descr=netif.name, + alias=netif.name, + type='6', + speed=interfaces.saveint(netif.speed)*1000000, + oper_status=netif.oper_status, + admin_status=netif.admin_status, + in_octets=interfaces.saveint(netif.rx_bytes), + in_ucast=interfaces.saveint(netif.rx_packets), + in_mcast=interfaces.saveint(netif.rx_multicast), + in_bcast=interfaces.saveint(netif.rx_broadcast), + in_discards=interfaces.saveint(netif.rx_dropped), + in_errors=interfaces.saveint(netif.rx_errors), + out_octets=interfaces.saveint(netif.tx_bytes), + out_ucast=interfaces.saveint(netif.tx_packets), + out_mcast=interfaces.saveint(netif.tx_multicast), + out_bcast=interfaces.saveint(netif.tx_broadcast), + out_discards=interfaces.saveint(netif.tx_dropped), + out_errors=interfaces.saveint(netif.tx_errors), + jumbo=True if netif.jumbo == "1" else False, + satisfaction=interfaces.saveint(netif.satisfaction) if netif.satisfaction and netif.oper_status == "1" else 0, + poe_enable=True if netif.poe_enable == "1" else False, + poe_mode=netif.poe_mode, + poe_current=float(netif.poe_current) if netif.poe_current else 0, + poe_voltage=float(netif.poe_voltage) if netif.poe_voltage else 0, + poe_power=float(netif.poe_power) if netif.poe_power else 0, + poe_class=netif.poe_class, + dot1x_mode=netif.dot1x_mode, + dot1x_status=netif.dot1x_status, + ip_address=netif.ip + ) for netif in parse_unifi_nested_dict(section).values() + ] + + + +def discovery_unifi_network_port_if( ## fixme parsed_section_name + params: Sequence[Mapping[str, Any]], + section: Section, +) -> DiscoveryResult: + yield from interfaces.discover_interfaces( + params, + _convert_unifi_counters_if(section), + ) + + +def check_unifi_network_port_if( ##fixme parsed_section_name + item: str, + params: Mapping[str, Any], + section: Section, +) -> CheckResult: + _converted_ifs = _convert_unifi_counters_if(section) + iface = next(filter(lambda x: item in (x.index,x.alias),_converted_ifs),None) ## fix Service Discovery appearance alias/descr + yield from interfaces.check_multiple_interfaces( + item, + params, + _converted_ifs, + ) + if iface: + yield Metric("satisfaction",max(0,iface.satisfaction)) + #pprint(iface) + if iface.poe_enable: + yield Result( + state=State.OK, + summary=f"PoE: {iface.poe_power}W" + ) + #yield Metric("poe_current",iface.poe_current) + yield Metric("poe_power",iface.poe_power) + yield Metric("poe_voltage",iface.poe_voltage) + if iface.ip_address: + yield Result( + state=State.OK, + summary=f"IP: {iface.ip_address}" + ) + + +register.check_plugin( + name='unifi_network_ports_if', + sections=["unifi_network_ports"], + service_name='Interface %s', + discovery_ruleset_name="inventory_if_rules", + discovery_ruleset_type=register.RuleSetType.ALL, + discovery_default_parameters=dict(interfaces.DISCOVERY_DEFAULT_PARAMETERS), + discovery_function=discovery_unifi_network_port_if, + check_ruleset_name="if", + check_default_parameters=interfaces.CHECK_DEFAULT_PARAMETERS, + check_function=check_unifi_network_port_if, +) +############ DEVICERADIO ########### +def discovery_unifi_radios(section): + #pprint(section) + for _radio in section.values(): + if _radio.radio == "ng": + yield Service(item="2.4Ghz") + if _radio.radio == "na": + yield Service(item="5Ghz") + +def check_unifi_radios(item,section): + _item = { "2.4Ghz" : "ng", "5Ghz" : "na" }.get(item) + radio = next(filter(lambda x: x.radio == _item,section.values())) + yield Metric("read_data",interfaces.saveint(radio.rx_bytes)) + yield Metric("write_data",interfaces.saveint(radio.tx_bytes)) + yield Metric("satisfaction",max(0,interfaces.saveint(radio.satisfaction))) + yield Metric("wlan_user_sta",interfaces.saveint(radio.user_num_sta)) + yield Metric("wlan_guest_sta",interfaces.saveint(radio.guest_num_sta)) + yield Metric("wlan_iot_sta",interfaces.saveint(radio.iot_num_sta)) + + yield Result( + state=State.OK, + summary=f"Channel: {radio.channel}" + ) + yield Result( + state=State.OK, + summary=f"Satisfaction: {radio.satisfaction}" + ) + yield Result( + state=State.OK, + summary=f"User: {radio.num_sta}" + ) + yield Result( + state=State.OK, + summary=f"Guest: {radio.guest_num_sta}" + ) + +register.agent_section( + name = 'unifi_network_radios', + parse_function = parse_unifi_nested_dict +) + +register.check_plugin( + name='unifi_network_radios', + service_name='Radio %s', + discovery_function=discovery_unifi_radios, + check_function=check_unifi_radios, +) + + +############ SSIDs ########### +def discovery_unifi_ssids(section): + for _ssid in section: + yield Service(item=_ssid) + +def check_unifi_ssids(item,section): + ssid = section.get(item) + _channels = ",".join(list(filter(lambda x: interfaces.saveint(x) > 0,[ssid.ng_channel,ssid.na_channel]))) + yield Result( + state=State.OK, + summary=f"Channels: {_channels}" + ) + if (interfaces.saveint(ssid.ng_is_guest) + interfaces.saveint(ssid.na_is_guest)) > 0: + yield Result( + state=State.OK, + summary="Guest" + ) + _satisfaction = max(0,min(interfaces.saveint(ssid.ng_satisfaction),interfaces.saveint(ssid.na_satisfaction))) + yield Result( + state=State.OK, + summary=f"Satisfaction: {_satisfaction}" + ) + _num_sta = interfaces.saveint(ssid.na_num_sta) + interfaces.saveint(ssid.ng_num_sta) + if _num_sta > 0: + yield Result( + state=State.OK, + summary=f"User: {_num_sta}" + ) + yield Metric("satisfaction",max(0,_satisfaction)) + yield Metric("wlan_24Ghz_num_user",interfaces.saveint(ssid.ng_num_sta) ) + yield Metric("wlan_5Ghz_num_user",interfaces.saveint(ssid.na_num_sta) ) + + yield Metric("na_avg_client_signal",interfaces.saveint(ssid.na_avg_client_signal)) + yield Metric("ng_avg_client_signal",interfaces.saveint(ssid.ng_avg_client_signal)) + + yield Metric("na_tcp_packet_loss",interfaces.saveint(ssid.na_tcp_packet_loss)) + yield Metric("ng_tcp_packet_loss",interfaces.saveint(ssid.ng_tcp_packet_loss)) + + yield Metric("na_wifi_retries",interfaces.saveint(ssid.na_wifi_retries)) + yield Metric("ng_wifi_retries",interfaces.saveint(ssid.ng_wifi_retries)) + yield Metric("na_wifi_latency",interfaces.saveint(ssid.na_wifi_latency)) + yield Metric("ng_wifi_latency",interfaces.saveint(ssid.ng_wifi_latency)) + + + +register.agent_section( + name = 'unifi_network_ssids', + parse_function = parse_unifi_nested_dict +) + +register.check_plugin( + name='unifi_network_ssids', + service_name='SSID: %s', + discovery_function=discovery_unifi_ssids, + check_function=check_unifi_ssids, +) + + +############ SSIDsListController ########### +def discovery_unifi_ssidlist(section): + #pprint(section) + for _ssid in section: + yield Service(item=_ssid) + +def check_unifi_ssidlist(item,section): + ssid = section.get(item) + yield Result( + state=State.OK, + summary=f"Channels: {ssid.channels}" + ) +# if (interfaces.saveint(ssid.ng_is_guest) + interfaces.saveint(ssid.na_is_guest)) > 0: +# yield Result( +# state=State.OK, +# summary="Guest" +# ) +# yield Result( +# state=State.OK, +# summary=f"Satisfaction: {_satisfaction}" +# ) + yield Result( + state=State.OK, + summary=f"User: {ssid.num_sta}" + ) + +register.agent_section( + name = 'unifi_ssid_list', + parse_function = parse_unifi_nested_dict +) + +register.check_plugin( + name='unifi_ssid_list', + service_name='SSID: %s', + discovery_function=discovery_unifi_ssidlist, + check_function=check_unifi_ssidlist, +) + + diff --git a/share/check_mk/agents/special/agent_unifi_controller b/share/check_mk/agents/special/agent_unifi_controller new file mode 100644 index 0000000..cbf650a --- /dev/null +++ b/share/check_mk/agents/special/agent_unifi_controller @@ -0,0 +1,480 @@ +#!/usr/bin/env python3 +# -*- encoding: utf-8; py-indent-offset: 4 -*- + +### +__VERSION__ = 0.75 + +import sys +import socket +import re +import json +import requests +from urllib3.exceptions import InsecureRequestWarning +from statistics import mean +from collections import defaultdict + +from pprint import pprint + + +try: + from cmk.special_agents.utils.argument_parsing import create_default_argument_parser + #from check_api import LOGGER ##/TODO +except ImportError: + from argparse import ArgumentParser as create_default_argument_parser + +class unifi_api_exception(Exception): + pass + +class unifi_object(object): + def __init__(self,**kwargs): + for _k,_v in kwargs.items(): + _k = _k.replace("-","_") + if type(_v) == bool: + _v = int(_v) + setattr(self,_k,_v) + + self._PARENT = kwargs.get("_PARENT",object) + if hasattr(self._PARENT,"_UNIFICONTROLLER"): + self._UNIFICONTROLLER = self._PARENT._UNIFICONTROLLER + self._API = self._PARENT._API + if hasattr(self,"_init"): + self._init() + +######################################## +###### +###### S S I D +###### +######################################## +class unifi_network_ssid(unifi_object): + def _init(self): + self._UNIFICONTROLLER._UNIFI_SSIDS.append(self) + self._UNIFI_SITE = self._PARENT._PARENT + for _k,_v in getattr(self,"reasons_bar_chart_now",{}).items(): + setattr(self,_k,_v) + def __str__(self): + _ret = [] + _unwanted = ["essid","radio","id","t","name","radio_name","wlanconf_id","is_wep","up","site_id","ap_mac","state"] + for _k,_v in self.__dict__.items(): + if _k.startswith("_") or _k in _unwanted or type(_v) not in (str,int,float): + continue + _ret.append(f"{self.essid}|{self.radio}_{_k}|{_v}") + return "\n".join(_ret) + +######################################## +###### +###### R A D I O +###### +######################################## +class unifi_network_radio(unifi_object): + def _update_stats(self,stats): + _prefixlen = len(self.name) +1 + for _k,_v in stats.items(): + if _k.startswith(self.name): + if type(_v) == float: + _v = int(_v) + setattr(self,_k[_prefixlen:],_v) + def __str__(self): + _ret = [] + _unwanted = ["name","ast_be_xmit","extchannel","cu_total","cu_self_rx","cu_self_tx"] + for _k,_v in self.__dict__.items(): + if _k.startswith("_") or _k in _unwanted or type(_v) not in (str,int,float): + continue + _ret.append(f"{self.name}|{_k}|{_v}") + return "\n".join(_ret) + +######################################## +###### +###### P O R T +###### +######################################## +class unifi_network_port(unifi_object): + def _init(self): + self.oper_status = self._get_state(getattr(self,"up",None)) + self.admin_status = self._get_state(getattr(self,"enable",None)) + if hasattr(self,"ifname"): ## GW / UDM Names + _name = list(filter(lambda x: x.get("ifname") == self.ifname,self._PARENT.ethernet_overrides)) + if _name: + _name = _name[0] + if getattr(self,"name",None) and _name.get("networkgroup") != "LAN": + self.name = _name.get("networkgroup","unkn") + else: + self.name = self.ifname + if not hasattr(self,"port_idx") and hasattr(self,"ifname"): + self.port_idx = int(self.ifname[-1])+1 ## ethX + + def _get_state(self,state): + return { + "1" : 1, ## up + "0" : 2 ## down + }.get(str(state),4) ##unknown + def __str__(self): + _ret = [] + _unwanted = ["up","enabled","media","anonymous_id","www_gw_mac","wan_gw_mac","attr_hidden_id","masked","flowctrl_tx","flowctrl_rx","portconf_id","speed_caps"] + for _k,_v in self.__dict__.items(): + if _k.startswith("_") or _k in _unwanted or type(_v) not in (str,int,float): + continue + _ret.append(f"{self.port_idx}|{_k}|{_v}") + return "\n".join(_ret) + +######################################## +###### +###### D E V I C E +###### +######################################## +class unifi_device(unifi_object): + def _init(self): + if not hasattr(self,"name"): + _mac_end = self.mac.replace(":","")[-4:] + self.name = f"{self.model}:{_mac_end}" + self._piggy_back = True + self._PARENT._SITE_DEVICES.append(self) + self._NETWORK_PORTS = [] + self._NETWORK_RADIO = [] + self._NETWORK_SSIDS = [] + + for _k,_v in getattr(self,"sys_stats",{}).items(): + _k = _k.replace("-","_") + setattr(self,_k,_v) + if self.type in ("ugw","udm"): + ## change ip to local ip + self.wan_ip = self.ip + self.ip = self.connect_request_ip + + if getattr(self,"speedtest_status_saved",False): + _speedtest = getattr(self,"speedtest_status",{}) + self.speedtest_time = int(_speedtest.get("rundate","0")) + self.speedtest_status = int(_speedtest.get("status_summary","0")) + self.speedtest_ping = round(_speedtest.get("latency",-1),1) + self.speedtest_download = round(_speedtest.get("xput_download",0.0),1) + self.speedtest_upload = round(_speedtest.get("xput_upload",0.0),1) + + _temp = list(map(lambda x: x.get("value",0),getattr(self,"temperatures",[]))) + if _temp: + self.general_temperature = "{0:.1f}".format(mean(_temp)) + + for _port in getattr(self,"port_table",[]): + self._NETWORK_PORTS.append(unifi_network_port(_PARENT=self,**_port)) + + for _radio in getattr(self,"radio_table_stats",[]): + _radio_obj = unifi_network_radio(_PARENT=self,**_radio) + _radio_obj._update_stats(getattr(self,"stat",{}).get("ap",{})) + self._NETWORK_RADIO.append(_radio_obj) + + for _ssid in getattr(self,"vap_table",[]): + self._NETWORK_SSIDS.append(unifi_network_ssid(_PARENT=self,**_ssid)) + + def _get_uplink(self): + if type(getattr(self,"uplink",None)) == dict: + self.uplink_up = int(self.uplink.get("up","0")) + self.uplink_device = self._UNIFICONTROLLER._get_device_by_mac(self.uplink.get("uplink_mac")) + self.uplink_remote_port = self.uplink.get("uplink_remote_port") + self.uplink_type = self.uplink.get("type") + + def _get_short_info(self): + _ret = [] + _wanted = ["version","ip","mac","serial","model","uptime","upgradeable","num_sta"] + for _k,_v in self.__dict__.items(): + if _k.startswith("_") or _k not in _wanted or type(_v) not in (str,int,float): + continue + _ret.append(f"{self.name}|{_k}|{_v}") + return "\n".join(_ret) + + def __str__(self): + if self._piggy_back: + _piggybackname = getattr(self,self._API.PIGGYBACK_ATTRIBUT,self.name) + _ret = [f"<<<<{_piggybackname}>>>>"] + else: + _ret = [] + _ret.append("<<>>") + _unwanted = ["anon_id","device_id","site_id","known_cfgversion","cfgversion","syslog_key","has_speaker","has_eth1", + "next_interval","next_heartbeat","next_heartbeat_at","guest_token","connect_request_ip","connect_request_port", + "start_connected_millis","start_disconnected_millis","wlangroup_id_na","wlangroup_id_ng","uplink_down_timeout" + "unsupported_reason","connected_at","provisioned_at","fw_caps","hw_caps","manufacturer_id","use_custom_config", + "led_override","led_override_color","led_override_color_brightness","sys_error_caps","adoptable_when_upgraded", + "mesh_uplink_1","mesh_uplink_1","considered_lost_at","outdoor_mode_override","unsupported_reason","architecture", + "kernel_version","required_version","prev_non_busy_state","has_fan","has_temperature","flowctrl_enabled","hash_id", + "speedtest-status-saved","usg_caps","two_phase_adopt","rollupgrade","locating","dot1x_portctrl_enabled", + "lcm_idle_timeout_override","lcm_brightness_override","uplink_depth","mesh_sta_vap_enabled","mesh_uplink_2", + "lcm_tracker_enabled","model_incompatible","model_in_lts","model_in_eol","country_code","wifi_caps", + "meshv3_peer_mac","element_peer_mac","vwireEnabled","hide_ch_width","x_authkey","x_ssh_hostkey_fingerprint", + "x_fingerprint","x_inform_authkey","op_mode" + ] + for _k,_v in self.__dict__.items(): + if _k.startswith("_") or _k in _unwanted or type(_v) not in (str,int,float): + continue + _ret.append(f"{_k}|{_v}") + + _ret.append("<<>>") + _ret.append(f"{{\"unifi_device\":\"unifi-{self.type}\"}}") + + if self._NETWORK_PORTS: + _ret += ["","<<>>"] + [str(_port) for _port in self._NETWORK_PORTS] + if self._NETWORK_RADIO: + _ret += ["","<<>>"] + [str(_radio) for _radio in self._NETWORK_RADIO] + + if self._NETWORK_SSIDS: + _ret += ["","<<>>"] + [str(_ssid) for _ssid in sorted(self._NETWORK_SSIDS,key=lambda x: x.essid)] + return "\n".join(_ret) + +######################################## +###### +###### S I T E +###### +######################################## +class unifi_site(unifi_object): + def _init(self): + for _subsys in self.health: + _name = _subsys.get("subsystem") + for _k,_v in _subsys.items(): + _k = _k.replace("-","_") + if _k == "subsystem" or type(_v) not in (str,int,float): + continue + #print(f"{_k}:{_v}") + setattr(self,f"{_name}_{_k}",_v) + + ##pprint(_api.get_data("/stat/rogueap")) + self._SITE_DEVICES = [] + self._get_devices() + _satisfaction = list(filter( + lambda x: x != None,map( + lambda x: getattr(x,"satisfaction",None),self._SITE_DEVICES + ) + )) + self.satisfaction = max(0,int(mean(_satisfaction)) if _satisfaction else 0) + + + def _get_devices(self): + _data = self._API.get_devices(site=self.name) + for _device in _data: + self._UNIFICONTROLLER._UNIFI_DEVICES.append(unifi_device(_PARENT=self,**_device)) + + def __str__(self): + _ret = ["<<>>"] + _unwanted = ["name","anonymous_id","www_gw_mac","wan_gw_mac","attr_hidden_id","attr_no_delete",""] + for _k,_v in self.__dict__.items(): + if _k.startswith("_") or _k in _unwanted or type(_v) not in (str,int,float): + continue + _ret.append(f"{self.name}|{_k}|{_v}") + return "\n".join(_ret) + +######################################## +###### +###### C O N T R O L L E R +###### +######################################## +class unifi_controller(unifi_object): + def _init(self): + self._UNIFICONTROLLER = self + self._UNIFI_SITES = [] + self._UNIFI_DEVICES = [] + self._UNIFI_SSIDS = [] + self._get_systemhealth() + self._get_sites() + for _dev in self._UNIFI_DEVICES: + _dev._get_uplink() + if hasattr(self,"cloudkey_version"): + self.cloudkey_version = re.sub(".*?v(\d+\.\d+\.\d+\.[a-z0-9]+).*","\\1",self.cloudkey_version) + self.type = getattr(self,"ubnt_device_type","unifi-sw-controller") + self.controller_version = self.version + delattr(self,"version") + + def _get_systemhealth(self): + _data = self._API.get_sysinfo() + _wanted = ["timezone","autobackup","version","previous_version","update_available","hostname","name","uptime","cloudkey_update_available","cloudkey_update_version","cloudkey_version","ubnt_device_type","udm_version","udm_update_version","udm_update_available"] + if _data: + for _k,_v in _data[0].items(): + if _k in _wanted: + if type(_v) == bool: + _v = int(_v) + setattr(self,_k,_v) + + def _get_device_by_mac(self,mac): + try: + return next(filter(lambda x: x.mac == mac,self._UNIFI_DEVICES)).name + except StopIteration: + return None + + def _get_sites(self): + _data = self._API.get_sites() + for _site in _data: + if self._API.SITES and _site.get("name") not in self._API.SITES and _site.get("desc").lower() not in self._API.SITES: + continue + self._UNIFI_SITES.append(unifi_site(_PARENT=self,**_site)) + + def _get_ssidlist(self): + _dict = defaultdict(list) + for _ssid in self._UNIFI_SSIDS: + _dict[f"{_ssid.essid}@{_ssid._UNIFI_SITE.desc}"].append(_ssid) + + _ret = [] + for _ssid,_obj in _dict.items(): + _ret.append("|".join([_ssid,"num_sta",str(sum(map(lambda x: getattr(x,"num_sta",0),_obj)))])) + _ret.append("|".join([_ssid,"channels",",".join( + sorted( + set(map(lambda x: str(getattr(x,"channel","0")),_obj)) + ,key = lambda x: int(x)) + )])) + _ret.append("|".join([_ssid,"avg_client_signal",str(mean(map(lambda x: getattr(x,"avg_client_signal",0),_obj))) ])) + return _ret + + def __str__(self): + _ret = ["<<>>"] + for _k,_v in self.__dict__.items(): + if _k.startswith("_") or type(_v) not in (str,int,float): + continue + _ret.append(f"{_k}|{_v}") + + ## check udm + _has_udm = list(filter(lambda x: x.name == self.name,self._UNIFI_DEVICES)) + if _has_udm: + _udm = _has_udm[0] + _udm._piggy_back = False + _ret.append(str(_udm)) + + _ret.append("<<>>") + _ret.append(f"{{\"unifi_device\":\"unifi-{self.type}\"}}") + + ## SITES ## + for _site in self._UNIFI_SITES: + _ret.append(str(_site)) + + _ret.append("<<>>") + for _device in self._UNIFI_DEVICES: + if _device._piggy_back: + _ret.append(_device._get_short_info()) + ## device list + + ## ssid list + _ret.append("<<>>") + _ret += self._get_ssidlist() + + if self._API.PIGGYBACK_ATTRIBUT.lower() != "none": + ## PIGGYBACK DEVICES ## + for _device in self._UNIFI_DEVICES: + if _device._piggy_back: + _ret.append(str(_device)) + return "\n".join(_ret) + + +######################################## +###### +###### A P I +###### https://ubntwiki.com/products/software/unifi-controller/api +######################################## +class unifi_controller_api(object): + def __init__(self,host,username,password,port,site,verify_cert,rawapi,piggybackattr,**kwargs): + self.host = host + self.url = f"https://{host}" + if port != 443: + self.url = f"https://{host}:{port}" + + self._verify_cert = verify_cert + if not verify_cert: + requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning) + self.RAW_API = rawapi + self.PIGGYBACK_ATTRIBUT = piggybackattr + self.SITES = site.lower().split(",") if site else None + self._session = requests.Session() + self.check_unifi_os() + self.login(username,password) + + def check_unifi_os(self): + _response = self.request("GET",url=self.url,allow_redirects=False) + self.is_unifios= _response.status_code == 200 and _response.headers.get("x-csrf-token") + + def get_sysinfo(self): + return self.get_data("/stat/sysinfo") + + def get_sites(self): + return self.get_data("/stat/sites",site=None) + + def get_devices(self,site): + return self.get_data("/stat/device",site=site) + + def login(self,username,password): + if self.is_unifios: + url=f"{self.url}/api/auth/login" + else: + url=f"{self.url}/api/login" + auth = { + "username" : username, + "password" : password, + "remember" : True + } + _response = self.request("POST",url=url,json=auth) + if _response.status_code == 404: + raise unifi_api_exception("API not Found try other Port or IP") + _json = _response.json() + if _json.get("meta",{}).get("rc") == "ok" or _json.get("status") == "ACTIVE": + return + raise unifi_api_exception("Login failed") + + def get_data(self,path,site="default",method="GET"): + _json = self.request(method=method,path=path,site=site).json() + _meta = _json.get("meta",{}) + if _meta.get("rc") == "ok": + return _json.get("data",[]) + raise unifi_api_exception(_meta.get("msg",_json.get("errors",repr(_json)))) + + def request(self,method,url=None,path=None,site=None,json=None,**kwargs): + if not url: + if self.is_unifios: + url = f"{self.url}/proxy/network/api/" + else: + url = f"{self.url}/api" + if site is not None: + url += f"/s/{site}" + if path is not None: + url += f"{path}" + _request = requests.Request(method,url,json=json) + _prepped_request = self._session.prepare_request(_request) + else: + _request = requests.Request(method,url,json=json) + _prepped_request = _request.prepare() + _response = self._session.send(_prepped_request,verify=self._verify_cert,timeout=10,**kwargs) + if _response.status_code == 200 and hasattr(_response,"json") and self.RAW_API: + try: + pprint(_response.json()) + except: + pass + return _response + +######################################## +###### +###### M A I N +###### +######################################## +if __name__ == '__main__': + parser = create_default_argument_parser(description=__doc__) + parser.add_argument('-u', '--user', dest='username', required=True, + help='User to access the DSM.') + parser.add_argument('-p', '--password', dest='password', required=True, + help='Password to access the DSM.') + parser.add_argument('--ignore-cert', dest='verify_cert', action='store_false', + help='Do not verify the SSL cert') + parser.add_argument('-s','--site', dest='site', required=False, + help='Site') + parser.add_argument('--port', dest='port',type=int,default='443') + parser.add_argument('--piggyback', dest='piggybackattr',type=str,default='name') + parser.add_argument('--rawapi', dest='rawapi', action='store_true') + parser.add_argument("host",type=str, + help="""Host name or IP address of Unifi Controller""") + args = parser.parse_args() + print("<<>>") + print(f"Version: {__VERSION__}") + try: + _api = unifi_controller_api(**args.__dict__) + except socket.error as e: + pprint(e) + sys.exit(1) + + if _api.is_unifios: + print("AgentOS: UnifiOS") + #pprint(_api.get_data("rest/portconf",site="default",method="GET")) + ##pprint(_api.get_data("/stat/rogueap")) + ##pprint(_api.get_data("/rest/user",site="default",method="GET")) + ##pprint(_api.get_data("/stat/sta",site="default",method="GET")) + #sys.exit(0) + _controller = unifi_controller(_API=_api) + if args.rawapi == False: + print(_controller) diff --git a/share/check_mk/checks/agent_unifi_controller b/share/check_mk/checks/agent_unifi_controller new file mode 100644 index 0000000..475b172 --- /dev/null +++ b/share/check_mk/checks/agent_unifi_controller @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- encoding: utf-8; py-indent-offset: 4 -*- + +#Function get params (in this case is port, passed via WATO agent rule cunfiguration, hostname and ip addres of host, +#for which agent will be invoked +def agent_unifi_controller_arguments(params, hostname, ipaddress): + args = [ + '--user', params['user'], + '--password', passwordstore_get_cmdline('%s', params['password']), + '--port', params['port'], + '--piggyback',params['piggyback'], + ] + _site = params.get("site") + if _site: + args += ["--site",_site] + if 'ignore_cert' in params and params['ignore_cert'] != '': + args += ['--ignore-cert'] + args += [ipaddress] + return args + + +#register invoke function for our agent +#key value for this dictionary is name part from register datasource of our agent (name="special_agents:myspecial") +special_agent_info['unifi_controller'] = agent_unifi_controller_arguments + + diff --git a/share/check_mk/inventory/unifi_controller b/share/check_mk/inventory/unifi_controller new file mode 100644 index 0000000..32b9be8 --- /dev/null +++ b/share/check_mk/inventory/unifi_controller @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# -*- encoding: utf-8; py-indent-offset: 4 -*- + +from pprint import pprint +from collections import defaultdict + +class dictobject(defaultdict): + def __getattr__(self,name): + return self[name] if name in self else "" + +nested_dictobject = lambda: dictobject(nested_dictobject) + +def inv_unifi_controller(info): + node = inv_tree("software.os") + node["version"] = info.get("controller_version") + +def inv_unifi_device(info): + node = inv_tree("software.configuration.snmp_info") + node["name"] = info.get("name") + node["contact"] = info.get("snmp_contact") + node["location"] = info.get("snmp_location") + node = inv_tree("software.os") + node["version"] = info.get("version") + node = inv_tree("harware.system") + node["vendor"] = "ubiquiti" + for _key in ("model","board_rev","serial","mac"): + _val = info.get(_key) + if _val: + node[_key] = _val + +def inv_unifi_port(info,params,inventory_tree): + _parsed = nested_dictobject() + for _line in info: + _parsed[_line[0]][_line[1]] = _line[2] + + _interfaces = [] + _total_ethernet_ports = 0 + _available_ethernet_ports = 0 + def _saveint(num): + try: + return int(num) + except (TypeError,ValueError): + return 0 + for _iface in _parsed.values(): + _interfaces.append({ + "index" : int(_iface.port_idx), + "description" : _iface.name, + "alias" : _iface.name, + "speed" : _saveint(_iface.speed)*1000000, + "phys_address" : "", + "oper_status" : _saveint(_iface.oper_status), + "admin_status" : _saveint(_iface.admin_status), + "port_type" : 6, + "available" : _iface.oper_status == '2' + }) + _total_ethernet_ports+=1 + _available_ethernet_ports+=1 if _iface.oper_status == '2' else 0 + + node = inventory_tree.get_list("networking.interfaces:") + node.extend(sorted(_interfaces, key=lambda i: i.get('index'))) + node = inventory_tree.get_dict("networking.") + node["available_ethernet_ports"] = _available_ethernet_ports + node["total_ethernet_ports"] = _total_ethernet_ports + node["total_interfaces"] = len(_parsed) + +inv_info["unifi_controller"] = { + "inv_function" : inv_unifi_controller +} +inv_info["unifi_device"] = { + "inv_function" : inv_unifi_device +} + +inv_info["unifi_network_ports"] = { + "inv_function" : inv_unifi_port +} + diff --git a/share/check_mk/web/plugins/metrics/unifi_metrics.py b/share/check_mk/web/plugins/metrics/unifi_metrics.py new file mode 100644 index 0000000..c29dc2a --- /dev/null +++ b/share/check_mk/web/plugins/metrics/unifi_metrics.py @@ -0,0 +1,209 @@ +from cmk.gui.i18n import _ +from cmk.gui.plugins.metrics import ( + metric_info, + graph_info, + translation +) + + +# Colors: +# +# red +# magenta orange +# 11 12 13 14 15 16 +# 46 21 +# 45 22 +# blue 44 23 yellow +# 43 24 +# 42 25 +# 41 26 +# 36 35 34 33 32 31 +# cyan yellow-green +# green +# +# Special colors: +# 51 gray +# 52 brown 1 +# 53 brown 2 +# +# For a new metric_info you have to choose a color. No more hex-codes are needed! +# Instead you can choose a number of the above color ring and a letter 'a' or 'b +# where 'a' represents the basic color and 'b' is a nuance/shading of the basic color. +# Both number and letter must be declared! +# +# Example: +# "color" : "23/a" (basic color yellow) +# "color" : "23/b" (nuance of color yellow) +# + + +metric_info["satisfaction"] = { + "title": _("Satisfaction"), + "unit": "%", + "color": "16/a", +} + +metric_info["poe_current"] = { + "title": _("PoE Current"), + "unit": "a", + "color": "16/a", +} +metric_info["poe_voltage"] = { + "title": _("PoE Voltage"), + "unit": "v", + "color": "12/a", +} +metric_info["poe_power"] = { + "title": _("PoE Power"), + "unit": "w", + "color": "16/a", +} + +metric_info["user_sta"] = { + "title": _("User"), + "unit": "", + "color": "13/b", +} +metric_info["guest_sta"] = { + "title": _("Guest"), + "unit": "", + "color": "13/a", +} + +graph_info["user_sta_combined"] = { + "title" : _("User"), + "metrics" : [ + ("user_sta","area"), + ("guest_sta","stack"), + ], +} + +metric_info["lan_user_sta"] = { + "title": _("LAN User"), + "unit": "count", + "color": "13/b", +} +metric_info["lan_guest_sta"] = { + "title": _("LAN Guest"), + "unit": "count", + "color": "13/a", +} + +graph_info["lan_user_sta_combined"] = { + "title" : _("LAN-User"), + "metrics" : [ + ("lan_user_sta","area"), + ("lan_guest_sta","stack"), + ], +} + +metric_info["wlan_user_sta"] = { + "title": _("WLAN User"), + "unit": "count", + "color": "13/b", +} +metric_info["wlan_guest_sta"] = { + "title": _("WLAN Guest"), + "unit": "count", + "color": "13/a", +} +metric_info["wlan_iot_sta"] = { + "title": _("WLAN IoT Devices"), + "unit": "count", + "color": "14/a", +} + +graph_info["wlan_user_sta_combined"] = { + "title" : _("WLAN-User"), + "metrics" : [ + ("wlan_user_sta","area"), + ("wlan_guest_sta","stack"), + ("wlan_iot_sta","stack"), + ], +} + +metric_info["wlan_24Ghz_num_user"] = { + "title": _("User 2.4Ghz"), + "unit": "count", + "color": "13/b", +} +metric_info["wlan_5Ghz_num_user"] = { + "title": _("User 5Ghz"), + "unit": "count", + "color": "13/a", +} + +graph_info["wlan_user_band_combined"] = { + "title" : _("WLAN User"), + "metrics" : [ + ("wlan_24Ghz_num_user","area"), + ("wlan_5Ghz_num_user","stack"), + ], +} + +#na_avg_client_signal +#ng_avg_client_signal + + +metric_info["wlan_if_in_octets"] = { + "title": _("Input Octets"), + "unit": "bytes/s", + "color": "#00e060", +} +metric_info["wlan_if_out_octets"] = { + "title": _("Output Octets"), + "unit": "bytes/s", + "color": "#00e060", +} +graph_info["wlan_bandwidth_translated"] = { + "title": _("Bandwidth WLAN"), + "metrics": [ + ("wlan_if_in_octets,8,*@bits/s", "area", _("Input bandwidth")), + ("wlan_if_out_octets,8,*@bits/s", "-area", _("Output bandwidth")), + ], + "scalars": [ + ("if_in_octets:warn", _("Warning (In)")), + ("if_in_octets:crit", _("Critical (In)")), + ("if_out_octets:warn,-1,*", _("Warning (Out)")), + ("if_out_octets:crit,-1,*", _("Critical (Out)")), + ], +} + + +metric_info["na_avg_client_signal"] = { + "title" :_("Average Signal 5Ghz"), + "unit" : "db", + "color" : "14/a", +} +metric_info["ng_avg_client_signal"] = { + "title" :_("Average Signal 2.4Ghz"), + "unit" : "db", + "color" : "#80f000", +} +graph_info["avg_client_signal_combined"] = { + "title" : _("Average Client Signal"), + "metrics" : [ + ("na_avg_client_signal","line"), + ("ng_avg_client_signal","line"), + ], + "range" : (-100,0) +} + + +## different unit ??? +#graph_info["poe_usage_combined"] = { +# "title" : _("PoE Usage"), +# "metrics" : [ +# ("poe_power","area"), +# ("poe_voltage","line"), +# ], +#} + +### fixme default uptime translation? +metric_info["unifi_uptime"] = { + "title" :_("Uptime"), + "unit" : "s", + "color" : "#80f000", +} + +check_metrics["check_mk-unifi_network_ports_if"] = translation.if_translation diff --git a/share/check_mk/web/plugins/perfometer/unifi_performeter.py b/share/check_mk/web/plugins/perfometer/unifi_performeter.py new file mode 100644 index 0000000..1c554c3 --- /dev/null +++ b/share/check_mk/web/plugins/perfometer/unifi_performeter.py @@ -0,0 +1,15 @@ +from cmk.gui.plugins.metrics import perfometer_info + +perfometer_info.append({ + "type": "linear", + "segments": ["satisfaction"], + "total": 100.0, +}) + +perfometer_info.append({ + "type": "logarithmic", + "metric": "unifi_uptime", + "half_value": 2592000.0, + "exponent": 2, +}) + diff --git a/share/check_mk/web/plugins/wato/datasource_unifi_controller.py b/share/check_mk/web/plugins/wato/datasource_unifi_controller.py new file mode 100644 index 0000000..2cf2457 --- /dev/null +++ b/share/check_mk/web/plugins/wato/datasource_unifi_controller.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- encoding: utf-8; py-indent-offset: 4 -*- + +from cmk.gui.i18n import _ +from cmk.gui.plugins.wato import ( + HostRulespec, + IndividualOrStoredPassword, + rulespec_registry, +) +from cmk.gui.valuespec import ( + Dictionary, + Alternative, + NetworkPort, + Checkbox, + TextAscii, +) + +from cmk.gui.plugins.wato.datasource_programs import RulespecGroupDatasourceProgramsHardware + +def _valuespec_special_agent_unifi_controller(): + return Dictionary( + title = _('Unifi Controller via API'), + help = _('This rule selects the Unifi API agent'), + optional_keys=['site'], + elements=[ + ('user',TextAscii(title = _('API Username.'),allow_empty = False,)), + ('password',IndividualOrStoredPassword(title = _('API password'),allow_empty = False,)), + ('site',TextAscii(title = _('Site Name'),allow_empty = False,default_value='default')), ## optional but not empty + ('port',NetworkPort(title = _('Port'),default_value = 443)), + ('ignore_cert', Checkbox(title=_("Ignore certificate validation"), default_value=False)), + ('piggyback', + Alternative( + title = _('Receive piggyback data by'), + elements = [ + FixedValue("name", title = _("Hostname")), + FixedValue("ip", title = _("IP")), + FixedValue("none", title = _("None")), + ], + default_value = "name" + ) + ) + ] + ) + +rulespec_registry.register( + HostRulespec( + group=RulespecGroupDatasourceProgramsHardware, + #IMPORTANT, name must follow special_agents:, + #where filename of our special agent located in path local/share/check_mk/agents/special/ is agent_ + name='special_agents:unifi_controller', + valuespec=_valuespec_special_agent_unifi_controller, + ))