From a9251b6dfc44268b5e47b375192803a7bc4e5ca6 Mon Sep 17 00:00:00 2001 From: Thorsten Spille Date: Mon, 28 Jun 2021 12:48:35 +0200 Subject: [PATCH] Added License, update to 0.79 --- .../plugins/agent_based/unifi_controller.py | 343 ++++++++++++++---- .../agents/special/agent_unifi_controller | 190 +++++++++- share/check_mk/checkman/unifi_controller | 13 + share/check_mk/checkman/unifi_device | 13 + .../check_mk/checkman/unifi_device_shortlist | 13 + .../check_mk/checkman/unifi_network_ports_if | 13 + share/check_mk/checkman/unifi_network_radios | 13 + share/check_mk/checkman/unifi_network_ssids | 13 + share/check_mk/checkman/unifi_sites | 13 + share/check_mk/checkman/unifi_ssid_list | 13 + share/check_mk/checks/agent_unifi_controller | 21 ++ .../web/plugins/metrics/unifi_metrics.py | 24 ++ .../plugins/perfometer/unifi_performeter.py | 24 ++ .../wato/datasource_unifi_controller.py | 22 ++ 14 files changed, 655 insertions(+), 73 deletions(-) create mode 100644 share/check_mk/checkman/unifi_controller create mode 100644 share/check_mk/checkman/unifi_device create mode 100644 share/check_mk/checkman/unifi_device_shortlist create mode 100644 share/check_mk/checkman/unifi_network_ports_if create mode 100644 share/check_mk/checkman/unifi_network_radios create mode 100644 share/check_mk/checkman/unifi_network_ssids create mode 100644 share/check_mk/checkman/unifi_sites create mode 100644 share/check_mk/checkman/unifi_ssid_list diff --git a/lib/check_mk/base/plugins/agent_based/unifi_controller.py b/lib/check_mk/base/plugins/agent_based/unifi_controller.py index 3b5c2b1..a22ded0 100644 --- a/lib/check_mk/base/plugins/agent_based/unifi_controller.py +++ b/lib/check_mk/base/plugins/agent_based/unifi_controller.py @@ -1,6 +1,28 @@ #!/usr/bin/env python3 # -*- encoding: utf-8; py-indent-offset: 4 -*- # +## MIT License +## +## Copyright (c) 2021 Bash Club +## +## Permission is hereby granted, free of charge, to any person obtaining a copy +## of this software and associated documentation files (the "Software"), to deal +## in the Software without restriction, including without limitation the rights +## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +## copies of the Software, and to permit persons to whom the Software is +## furnished to do so, subject to the following conditions: +## +## The above copyright notice and this permission notice shall be included in all +## copies or substantial portions of the Software. +## +## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +## SOFTWARE. + from cmk.gui.i18n import _ from .agent_based_api.v1 import ( @@ -8,9 +30,13 @@ from .agent_based_api.v1 import ( register, render, Result, + IgnoreResults, Service, State, + TableRow, + Attributes ) + from .agent_based_api.v1.type_defs import CheckResult, DiscoveryResult from typing import Any, Dict, Mapping, Sequence, Optional @@ -38,6 +64,12 @@ def _safe_float(val): except (TypeError,ValueError): return 0 +def _safe_int(val,default=0): + try: + return int(val) + except (TypeError,ValueError): + return default + def _unifi_status2state(status): return { "ok" : State.OK, @@ -59,6 +91,13 @@ def parse_unifi_nested_dict(string_table): _ret[_line[0]][_line[1]] = _line[2] return _ret +UNIFI_DEVICE_STATES = { + "0" : "disconnected", + "1" : "connected", + "2" : "pending" +} + + ############ Controller ############ def discovery_unifi_controller(section): yield Service(item="Unifi Controller") @@ -88,6 +127,14 @@ def check_unifi_controller(item,section): notice=_("Update available") ) +def inventory_unifi_controller(section): + yield Attributes( + path=["software","os"], + inventory_attributes={ + "controller_version" : section.get("controller_version") + } + ) + register.agent_section( name = 'unifi_controller', parse_function = parse_unifi_dict @@ -99,6 +146,12 @@ register.check_plugin( discovery_function=discovery_unifi_controller, check_function=check_unifi_controller, ) + +register.inventory_plugin( + name = "unifi_controller", + inventory_function = inventory_unifi_controller +) + ############ SITES ########### def discovery_unifi_sites(section): @@ -107,15 +160,15 @@ def discovery_unifi_sites(section): 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))) + yield Metric("satisfaction",max(0,_safe_int(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 Metric("lan_user_sta",_safe_int(site.lan_num_user)) + yield Metric("lan_guest_sta",_safe_int(site.lan_num_guest)) + yield Metric("if_in_octets",_safe_int(site.lan_rx_bytes_r)) + #yield Metric("if_in_bps",_safe_int(site.lan_rx_bytes_r)*8) + yield Metric("if_out_octets",_safe_int(site.lan_tx_bytes_r)) + #yield Metric("if_out_bps",_safe_int(site.lan_tx_bytes_r)*8) yield Result( state=_unifi_status2state(site.lan_status), @@ -127,11 +180,11 @@ def check_unifi_sites(item,section): #) 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 Metric("wlan_user_sta",_safe_int(site.wlan_num_user)) + yield Metric("wlan_guest_sta",_safe_int(site.wlan_num_guest)) + yield Metric("wlan_iot_sta",_safe_int(site.wlan_num_iot)) + yield Metric("wlan_if_in_octets",_safe_int(site.wlan_rx_bytes_r)) + yield Metric("wlan_if_out_octets",_safe_int(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})" @@ -173,6 +226,36 @@ register.check_plugin( check_function=check_unifi_sites, ) +############ DEVICE_SHORTLIST ########## + +def inventory_unifi_device_shortlist(section): + for _name,_device in section.items(): + yield TableRow( + path=["hardware","networkdevices"], + key_columns={"_name" : _name}, + inventory_columns={ + "serial" : _device.get("serial"), + "_state" : UNIFI_DEVICE_STATES.get(_device.state,"unknown"), + "vendor" : "ubiquiti", + "model" : _device.get("model_name",_device.get("model")), + "version" : _device.version, + "ip_address": _device.ip, + "mac" : _device.mac + } + ) + +register.agent_section( + name = 'unifi_device_shortlist', + parse_function = parse_unifi_nested_dict +) + +register.inventory_plugin( + name = "unifi_device_shortlist", + inventory_function = inventory_unifi_device_shortlist +) + + + ############ DEVICE ########### def discovery_unifi_device(section): yield Service(item="Unifi Device") @@ -188,29 +271,32 @@ def discovery_unifi_device(section): yield Service(item="Speedtest") def check_unifi_device(item,section): + if section.state != "1": + yield IgnoreResults(f"device not active State: {section.state}") + if item == "Unifi Device": yield Result( state=State.OK, summary=f"Version: {section.version}" ) - if interfaces.saveint(section.upgradable) > 0: + if _safe_int(section.upgradable) > 0: yield Result( state=State.WARN, notice=_("Update available") ) if item == "Active-User": - _active_user = interfaces.saveint(section.user_num_sta) + _active_user = _safe_int(section.user_num_sta) yield Result( state=State.OK, summary=f"{_active_user}" ) - if interfaces.saveint(section.guest_num_sta) > -1: + if _safe_int(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)) + yield Metric("guest_sta",_safe_int(section.guest_num_sta)) if item == "Uptime": _uptime = int(section.uptime) if section.uptime else -1 if _uptime > 0: @@ -224,7 +310,7 @@ def check_unifi_device(item,section): state=State.OK, summary=f"{section.satisfaction}%" ) - yield Metric("satisfaction",max(0,interfaces.saveint(section.satisfaction))) + yield Metric("satisfaction",max(0,_safe_int(section.satisfaction))) if item == "Temperature": yield Metric("temp",_safe_float(section.general_temperature)) yield Result( @@ -264,6 +350,33 @@ def check_unifi_device(item,section): summary=f"Device {section.uplink_device} Port: {section.uplink_remote_port}" ) +def inventory_unifi_device(section): + yield Attributes( + path=["software","os"], + inventory_attributes={ + "version" : section.get("version") + } + ) + yield Attributes( + path=["software","configuration","snmp_info"], + inventory_attributes = { + "name" : section.get("name"), + "contact" : section.get("snmp_contact"), + "location" : section.get("snmp_location") + } + ) + _hwdict = { + "vendor" : "ubiquiti", + } + for _key in ("model","board_rev","serial","mac"): + _val = section.get(_key) + if _val: + _hwdict[_key] = _val + yield Attributes( + path=["hardware","system"], + inventory_attributes= _hwdict + ) + register.agent_section( name = 'unifi_device', parse_function = parse_unifi_dict @@ -275,6 +388,12 @@ register.check_plugin( discovery_function=discovery_unifi_device, check_function=check_unifi_device, ) + +register.inventory_plugin( + name = "unifi_device", + inventory_function = inventory_unifi_device +) + ############ DEVICEPORT ########### @dataclass class unifi_interface(interfaces.Interface): @@ -290,34 +409,87 @@ class unifi_interface(interfaces.Interface): dot1x_mode : Optional[str] = None dot1x_status : Optional[str] = None ip_address : Optional[str] = None + portconf : Optional[str] = None def __post_init__(self) -> None: self.finalize() def _convert_unifi_counters_if(section: Section) -> interfaces.Section: + ## 10|port_idx|10 + ## 10|port_poe|1 + ## 10|poe_caps|7 + ## 10|op_mode|switch + ## 10|poe_mode|auto + ## 10|anomalies|0 + ## 10|autoneg|1 + ## 10|dot1x_mode|unknown + ## 10|dot1x_status|disabled + ## 10|enable|1 + ## 10|full_duplex|1 + ## 10|is_uplink|0 + ## 10|jumbo|1 + ## 10|poe_class|Unknown + ## 10|poe_current|0.00 + ## 10|poe_enable|0 + ## 10|poe_good|0 + ## 10|poe_power|0.00 + ## 10|poe_voltage|0.00 + ## 10|rx_broadcast|1290 + ## 10|rx_bytes|38499976384 + ## 10|rx_dropped|0 + ## 10|rx_errors|0 + ## 10|rx_multicast|16423 + ## 10|rx_packets|125489263 + ## 10|satisfaction|100 + ## 10|satisfaction_reason|0 + ## 10|speed|1000 + ## 10|stp_pathcost|20000 + ## 10|stp_state|forwarding + ## 10|tx_broadcast|20791854 + ## 10|tx_bytes|238190158091 + ## 10|tx_dropped|0 + ## 10|tx_errors|0 + ## 10|tx_multicast|262691 + ## 10|tx_packets|228482694 + ## 10|tx_bytes_r|17729 + ## 10|rx_bytes_r|176941 + ## 10|bytes_r|194671 + ## 10|name|Port 10 + ## 10|aggregated_by|0 + ## 10|oper_status|1 + ## 10|admin_status|1 + ## 10|portconf|ALL + ## unifi_interface(index='10', descr='Port 10', alias='Port 10', type='6', speed=1000000000, oper_status='1', + ## in_octets=38448560321, in_ucast=125404491, in_mcast=16414, in_bcast=1290, in_discards=0, in_errors=0, + ## out_octets=238185160794, out_ucast=228451699, out_mcast=262551, out_bcast=20783341, out_discards=0, out_errors=0, + ## out_qlen=0, phys_address='', oper_status_name='up', speed_as_text='', group=None, node=None, admin_status='1', + ## total_octets=276633721115, jumbo=True, satisfaction=100, + ## poe_enable=False, poe_mode='auto', poe_good=None, poe_current=0.0, poe_power=0.0, poe_voltage=0.0, poe_class='Unknown', + ## dot1x_mode='unknown',dot1x_status='disabled', ip_address='', portconf='ALL') + return [ unifi_interface( index=str(netif.port_idx), descr=netif.name, alias=netif.name, type='6', - speed=interfaces.saveint(netif.speed)*1000000, + speed=_safe_int(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), + in_octets=_safe_int(netif.rx_bytes), + in_ucast=_safe_int(netif.rx_packets), + in_mcast=_safe_int(netif.rx_multicast), + in_bcast=_safe_int(netif.rx_broadcast), + in_discards=_safe_int(netif.rx_dropped), + in_errors=_safe_int(netif.rx_errors), + out_octets=_safe_int(netif.tx_bytes), + out_ucast=_safe_int(netif.tx_packets), + out_mcast=_safe_int(netif.tx_multicast), + out_bcast=_safe_int(netif.tx_broadcast), + out_discards=_safe_int(netif.tx_dropped), + out_errors=_safe_int(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, + satisfaction=_safe_int(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, @@ -326,7 +498,8 @@ def _convert_unifi_counters_if(section: Section) -> interfaces.Section: poe_class=netif.poe_class, dot1x_mode=netif.dot1x_mode, dot1x_status=netif.dot1x_status, - ip_address=netif.ip + ip_address=netif.ip, + portconf=netif.portconf ) for netif in parse_unifi_nested_dict(section).values() ] @@ -348,13 +521,19 @@ def check_unifi_network_port_if( ##fixme parsed_section_name 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 + iface = next(filter(lambda x: _safe_int(item,-1) == _safe_int(x.index) or item == x.alias,_converted_ifs),None) ## fix Service Discovery appearance alias/descr yield from interfaces.check_multiple_interfaces( item, params, _converted_ifs, ) if iface: + #pprint(iface) + if iface.portconf: + yield Result( + state=State.OK, + summary=f"Network: {iface.portconf}" + ) yield Metric("satisfaction",max(0,iface.satisfaction)) #pprint(iface) if iface.poe_enable: @@ -371,6 +550,35 @@ def check_unifi_network_port_if( ##fixme parsed_section_name summary=f"IP: {iface.ip_address}" ) +def inventory_unifi_network_ports(section): + _total_ethernet_ports = 0 + _available_ethernet_ports = 0 + for _iface in parse_unifi_nested_dict(section).values(): + _total_ethernet_ports +=1 + _available_ethernet_ports +=1 if _iface.oper_status == '2' else 0 + yield TableRow( + path=["networking","interfaces"], + key_columns={"index" : _safe_int(_iface.port_idx)}, + inventory_columns={ + "description" : _iface.name, + "alias" : _iface.name, + "speed" : _safe_int(_iface.speed)*1000000, + "oper_status" : _safe_int(_iface.oper_status), + "admin_status" : _safe_int(_iface.admin_status), + "available" : _iface.oper_status == '2', + "vlans" : _iface.portconf, + "port_type" : 6, + } + ) + + yield Attributes( + path=["networking"], + inventory_attributes={ + "available_ethernet_ports" : _available_ethernet_ports, + "total_ethernet_ports" : _total_ethernet_ports, + "total_interfaces" : _total_ethernet_ports + } + ) register.check_plugin( name='unifi_network_ports_if', @@ -384,6 +592,12 @@ register.check_plugin( check_default_parameters=interfaces.CHECK_DEFAULT_PARAMETERS, check_function=check_unifi_network_port_if, ) + +register.inventory_plugin( + name = "unifi_network_ports", + inventory_function = inventory_unifi_network_ports +) + ############ DEVICERADIO ########### def discovery_unifi_radios(section): #pprint(section) @@ -396,12 +610,12 @@ def discovery_unifi_radios(section): 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 Metric("read_data",_safe_int(radio.rx_bytes)) + yield Metric("write_data",_safe_int(radio.tx_bytes)) + yield Metric("satisfaction",max(0,_safe_int(radio.satisfaction))) + yield Metric("wlan_user_sta",_safe_int(radio.user_num_sta)) + yield Metric("wlan_guest_sta",_safe_int(radio.guest_num_sta)) + yield Metric("wlan_iot_sta",_safe_int(radio.iot_num_sta)) yield Result( state=State.OK, @@ -440,41 +654,41 @@ def discovery_unifi_ssids(section): 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]))) + _channels = ",".join(list(filter(lambda x: _safe_int(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: + if (_safe_int(ssid.ng_is_guest) + _safe_int(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))) + _satisfaction = max(0,min(_safe_int(ssid.ng_satisfaction),_safe_int(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) + _num_sta = _safe_int(ssid.na_num_sta) + _safe_int(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("wlan_24Ghz_num_user",_safe_int(ssid.ng_num_sta) ) + yield Metric("wlan_5Ghz_num_user",_safe_int(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_avg_client_signal",_safe_int(ssid.na_avg_client_signal)) + yield Metric("ng_avg_client_signal",_safe_int(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_tcp_packet_loss",_safe_int(ssid.na_tcp_packet_loss)) + yield Metric("ng_tcp_packet_loss",_safe_int(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)) + yield Metric("na_wifi_retries",_safe_int(ssid.na_wifi_retries)) + yield Metric("ng_wifi_retries",_safe_int(ssid.ng_wifi_retries)) + yield Metric("na_wifi_latency",_safe_int(ssid.na_wifi_latency)) + yield Metric("ng_wifi_latency",_safe_int(ssid.ng_wifi_latency)) @@ -503,19 +717,22 @@ def check_unifi_ssidlist(item,section): 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}" ) + yield Metric("wlan_24Ghz_num_user",_safe_int(ssid.ng_num_sta) ) + yield Metric("wlan_5Ghz_num_user",_safe_int(ssid.na_num_sta) ) + yield Metric("na_avg_client_signal",_safe_int(ssid.na_avg_client_signal)) + yield Metric("ng_avg_client_signal",_safe_int(ssid.ng_avg_client_signal)) + + yield Metric("na_tcp_packet_loss",_safe_int(ssid.na_tcp_packet_loss)) + yield Metric("ng_tcp_packet_loss",_safe_int(ssid.ng_tcp_packet_loss)) + + yield Metric("na_wifi_retries",_safe_int(ssid.na_wifi_retries)) + yield Metric("ng_wifi_retries",_safe_int(ssid.ng_wifi_retries)) + yield Metric("na_wifi_latency",_safe_int(ssid.na_wifi_latency)) + yield Metric("ng_wifi_latency",_safe_int(ssid.ng_wifi_latency)) register.agent_section( name = 'unifi_ssid_list', @@ -530,3 +747,5 @@ register.check_plugin( ) + + diff --git a/share/check_mk/agents/special/agent_unifi_controller b/share/check_mk/agents/special/agent_unifi_controller index cbf650a..621d50f 100644 --- a/share/check_mk/agents/special/agent_unifi_controller +++ b/share/check_mk/agents/special/agent_unifi_controller @@ -1,8 +1,29 @@ #!/usr/bin/env python3 # -*- encoding: utf-8; py-indent-offset: 4 -*- +## MIT License +## +## Copyright (c) 2021 Bash Club +## +## Permission is hereby granted, free of charge, to any person obtaining a copy +## of this software and associated documentation files (the "Software"), to deal +## in the Software without restriction, including without limitation the rights +## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +## copies of the Software, and to permit persons to whom the Software is +## furnished to do so, subject to the following conditions: +## +## The above copyright notice and this permission notice shall be included in all +## copies or substantial portions of the Software. +## +## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +## SOFTWARE. ### -__VERSION__ = 0.75 +__VERSION__ = 0.79 import sys import socket @@ -14,7 +35,127 @@ from statistics import mean from collections import defaultdict from pprint import pprint +try: + import cmk.utils.paths + AGENT_TMP_PATH = cmk.utils.paths.Path(cmk.utils.paths.tmp_dir, "agents/agent_unifi") +except ImportError: + AGENT_TMP_PATH = None +UNIFI_DEVICE_TABLE = { + 'BZ2' : 'UAP', + 'BZ2LR' : 'UAP-LR', + 'U2HSR' : 'UAP-Outdoor+', + 'U2IW' : 'UAP-IW', + 'U2L48' : 'UAP-AC-LR', + 'U2Lv2' : 'UAP-AC-LR', + 'U2M' : 'UAP-Mini', + 'U2O' : 'UAP-Outdoor', + 'U2S48' : 'UAP-AC', + 'U2Sv2' : 'UAP-AC', + 'U5O' : 'UAP-Outdoor5', + 'U7E' : 'UAP-AC', + 'U7EDU' : 'UAP-AC-EDU', + 'U7Ev2' : 'UAP-AC', + 'U7HD' : 'UAP-AC-HD', + 'U7SHD' : 'UAP-AC-SHD', + 'U7NHD' : 'UAP-nanoHD', + 'UFLHD' : 'UAP-FlexHD', + 'UHDIW' : 'UAP-IW-HD', + 'UAIW6' : 'U6-IW', + 'UAE6' : 'U6-Extender', + 'UAL6' : 'U6-Lite', + 'UAM6' : 'U6-Mesh', + 'UALR6' : 'U6-LR-EA', + 'UAP6' : 'U6-LR', + 'UALR6v2' : 'U6-LR', + 'UALR6v3' : 'U6-LR', + 'UCXG' : 'UAP-XG', + 'UXSDM' : 'UWB-XG', + 'UXBSDM' : 'UWB-XG-BK', + 'UCMSH' : 'UAP-XG-Mesh', + 'U7IW' : 'UAP-AC-IW', + 'U7IWP' : 'UAP-AC-IW-Pro', + 'U7MP' : 'UAP-AC-M-Pro', + 'U7LR' : 'UAP-AC-LR', + 'U7LT' : 'UAP-AC-Lite', + 'U7O' : 'UAP-AC-Outdoor', + 'U7P' : 'UAP-Pro', + 'U7MSH' : 'UAP-AC-M', + 'U7PG2' : 'UAP-AC-Pro', + 'p2N' : 'PICOM2HP', + 'UDMB' : 'UAP-BeaconHD', + 'USF5P' : 'USW-Flex', + 'US8' : 'US-8', + 'US8P60' : 'US-8-60W', + 'US8P150' : 'US-8-150W', + 'S28150' : 'US-8-150W', + 'USC8' : 'US-8', + 'USC8P60' : 'US-8-60W', + 'USC8P150' : 'US-8-150W', + 'US16P150' : 'US-16-150W', + 'S216150' : 'US-16-150W', + 'US24' : 'US-24-G1', + 'US24PRO' : 'USW-Pro-24-PoE', + 'US24PRO2' : 'USW-Pro-24', + 'US24P250' : 'US-24-250W', + 'US24PL2' : 'US-L2-24-PoE', + 'US24P500' : 'US-24-500W', + 'S224250' : 'US-24-250W', + 'S224500' : 'US-24-500W', + 'US48' : 'US-48-G1', + 'US48PRO' : 'USW-Pro-48-PoE', + 'US48PRO2' : 'USW-Pro-48', + 'US48P500' : 'US-48-500W', + 'US48PL2' : 'US-L2-48-PoE', + 'US48P750' : 'US-48-750W', + 'S248500' : 'US-48-500W', + 'S248750' : 'US-48-750W', + 'US6XG150' : 'US-XG-6PoE', + 'USMINI' : 'USW-Flex-Mini', + 'USXG' : 'US-16-XG', + 'USC8P450' : 'USW-Industrial', + 'UDC48X6' : 'USW-Leaf', + 'USL8A' : 'UniFi Switch Aggregation', + 'USAGGPRO' : 'UniFi Switch Aggregation Pro', + 'USL8LP' : 'USW-Lite-8-PoE', + 'USL8MP' : 'USW-Mission-Critical', + 'USL16P' : 'USW-16-PoE', + 'USL16LP' : 'USW-Lite-16-PoE', + 'USL24' : 'USW-24-G2', + 'USL48' : 'USW-48-G2', + 'USL24P' : 'USW-24-PoE', + 'USL48P' : 'USW-48-PoE', + 'UGW3' : 'USG-3P', + 'UGW4' : 'USG-Pro-4', + 'UGWHD4' : 'USG', + 'UGWXG' : 'USG-XG-8', + 'UDM' : 'UDM', + 'UDMSE' : 'UDM-SE', + 'UDMPRO' : 'UDM-Pro', + 'UP4' : 'UVP-X', + 'UP5' : 'UVP', + 'UP5t' : 'UVP-Pro', + 'UP7' : 'UVP-Executive', + 'UP5c' : 'UVP', + 'UP5tc' : 'UVP-Pro', + 'UP7c' : 'UVP-Executive', + 'UCK' : 'UCK', + 'UCK-v2' : 'UCK', + 'UCK-v3' : 'UCK', + 'UCKG2' : 'UCK-G2', + 'UCKP' : 'UCK-G2-Plus', + 'UASXG' : 'UAS-XG', + 'ULTE' : 'U-LTE', + 'ULTEPUS' : 'U-LTE-Pro', + 'ULTEPEU' : 'U-LTE-Pro', + 'UP1' : 'USP-Plug', + 'UP6' : 'USP-Strip', + 'USPPDUP' : 'USP - Power Distribution Unit Pro', + 'USPRPS' : 'USP-RPS', + 'US624P' : 'UniFi6 Switch 24', + 'UBB' : 'UBB', + 'UXGPRO' : 'UniFi NeXt-Gen Gateway PRO' +} try: from cmk.special_agents.utils.argument_parsing import create_default_argument_parser @@ -40,6 +181,9 @@ class unifi_object(object): if hasattr(self,"_init"): self._init() + def __repr__(self): + return repr([(_k,_v) for _k,_v in self.__dict__.items() if type(_v) in (int,str)]) + ######################################## ###### ###### S S I D @@ -51,9 +195,17 @@ class unifi_network_ssid(unifi_object): self._UNIFI_SITE = self._PARENT._PARENT for _k,_v in getattr(self,"reasons_bar_chart_now",{}).items(): setattr(self,_k,_v) + setattr(self,f"{self.radio}_num_sta",self.num_sta) + setattr(self,f"{self.radio}_tcp_packet_loss",self.tcp_packet_loss) + setattr(self,f"{self.radio}_wifi_retries",self.wifi_retries) + setattr(self,f"{self.radio}_wifi_latency",self.wifi_latency) + setattr(self,f"{self.radio}_avg_client_signal",self.avg_client_signal) def __str__(self): _ret = [] - _unwanted = ["essid","radio","id","t","name","radio_name","wlanconf_id","is_wep","up","site_id","ap_mac","state"] + _unwanted = ["essid","radio","id","t","name","radio_name","wlanconf_id","is_wep","up","site_id","ap_mac","state", + "na_num_sta","ng_num_sta","ng_tcp_packet_loss","na_tcp_packet_loss","na_wifi_retries","ng_wifi_retries", + "na_wifi_latency","ng_wifi_latency","na_avg_client_signal","ng_avg_client_signal" + ] for _k,_v in self.__dict__.items(): if _k.startswith("_") or _k in _unwanted or type(_v) not in (str,int,float): continue @@ -101,6 +253,9 @@ class unifi_network_port(unifi_object): self.name = self.ifname if not hasattr(self,"port_idx") and hasattr(self,"ifname"): self.port_idx = int(self.ifname[-1])+1 ## ethX + + self.portconf = self._PARENT._PARENT._PORTCONFIGS.get(getattr(self,"portconf_id",None)) + def _get_state(self,state): return { @@ -135,6 +290,7 @@ class unifi_device(unifi_object): for _k,_v in getattr(self,"sys_stats",{}).items(): _k = _k.replace("-","_") setattr(self,_k,_v) + self.model_name = UNIFI_DEVICE_TABLE.get(self.model) if self.type in ("ugw","udm"): ## change ip to local ip self.wan_ip = self.ip @@ -172,7 +328,7 @@ class unifi_device(unifi_object): def _get_short_info(self): _ret = [] - _wanted = ["version","ip","mac","serial","model","uptime","upgradeable","num_sta"] + _wanted = ["version","ip","mac","serial","model","model_name","uptime","upgradeable","num_sta","adopted","state"] for _k,_v in self.__dict__.items(): if _k.startswith("_") or _k not in _wanted or type(_v) not in (str,int,float): continue @@ -234,6 +390,8 @@ class unifi_site(unifi_object): ##pprint(_api.get_data("/stat/rogueap")) self._SITE_DEVICES = [] + self._PORTCONFIGS = {} + self._get_portconfig() self._get_devices() _satisfaction = list(filter( lambda x: x != None,map( @@ -242,6 +400,10 @@ class unifi_site(unifi_object): )) self.satisfaction = max(0,int(mean(_satisfaction)) if _satisfaction else 0) + def _get_portconfig(self): + _data = self._API.get_portconfig(site=self.name) + for _config in _data: + self._PORTCONFIGS[_config["_id"]] = _config.get("name") def _get_devices(self): _data = self._API.get_devices(site=self.name) @@ -308,7 +470,11 @@ class unifi_controller(unifi_object): _ret = [] for _ssid,_obj in _dict.items(): - _ret.append("|".join([_ssid,"num_sta",str(sum(map(lambda x: getattr(x,"num_sta",0),_obj)))])) + pprint(_obj) + for _key in ("num_sta","ng_num_sta","na_num_sta","ng_tcp_packet_loss","na_tcp_packet_loss","ng_wifi_retries","na_wifi_retries","ng_wifi_latency","na_wifi_latency"): + _ret.append("|".join([_ssid,_key,str(sum(map(lambda x: getattr(x,_key,0),_obj)))])) + _ret.append("|".join([_ssid,"ng_avg_client_signal",str(mean(map(lambda x: getattr(x,"ng_avg_client_signal",0),filter(lambda x: x.radio == "ng",_obj))))])) + _ret.append("|".join([_ssid,"na_avg_client_signal",str(mean(map(lambda x: getattr(x,"na_avg_client_signal",0),filter(lambda x: x.radio == "na",_obj))))])) _ret.append("|".join([_ssid,"channels",",".join( sorted( set(map(lambda x: str(getattr(x,"channel","0")),_obj)) @@ -338,7 +504,7 @@ class unifi_controller(unifi_object): for _site in self._UNIFI_SITES: _ret.append(str(_site)) - _ret.append("<<>>") + _ret.append("<<>>") for _device in self._UNIFI_DEVICES: if _device._piggy_back: _ret.append(_device._get_short_info()) @@ -351,7 +517,7 @@ class unifi_controller(unifi_object): if self._API.PIGGYBACK_ATTRIBUT.lower() != "none": ## PIGGYBACK DEVICES ## for _device in self._UNIFI_DEVICES: - if _device._piggy_back: + if _device._piggy_back and _device.adopted: _ret.append(str(_device)) return "\n".join(_ret) @@ -388,6 +554,9 @@ class unifi_controller_api(object): def get_sites(self): return self.get_data("/stat/sites",site=None) + def get_portconfig(self,site): + return self.get_data("/rest/portconf",site=site) + def get_devices(self,site): return self.get_data("/stat/device",site=site) @@ -409,8 +578,8 @@ class unifi_controller_api(object): 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() + def get_data(self,path,site="default",method="GET",**kwargs): + _json = self.request(method=method,path=path,site=site,**kwargs).json() _meta = _json.get("meta",{}) if _meta.get("rc") == "ok": return _json.get("data",[]) @@ -470,11 +639,10 @@ if __name__ == '__main__': 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("/stat/rogueap?within=4")) ##pprint(_api.get_data("/rest/user",site="default",method="GET")) ##pprint(_api.get_data("/stat/sta",site="default",method="GET")) - #sys.exit(0) + ##sys.exit(0) _controller = unifi_controller(_API=_api) if args.rawapi == False: print(_controller) diff --git a/share/check_mk/checkman/unifi_controller b/share/check_mk/checkman/unifi_controller new file mode 100644 index 0000000..66a19eb --- /dev/null +++ b/share/check_mk/checkman/unifi_controller @@ -0,0 +1,13 @@ +title: Unifi Controller +agents: unifi_controller +catalog: networking +licence: MIT +description: + plz fill me + +item: + The name of the device + +inventory: + One Service for each device + \ No newline at end of file diff --git a/share/check_mk/checkman/unifi_device b/share/check_mk/checkman/unifi_device new file mode 100644 index 0000000..c83defd --- /dev/null +++ b/share/check_mk/checkman/unifi_device @@ -0,0 +1,13 @@ +title: Unifi Device +agents: unifi_controller +catalog: networking +licence: MIT +description: + plz fill me + +item: + The name of the device + +inventory: + One Service for each device + \ No newline at end of file diff --git a/share/check_mk/checkman/unifi_device_shortlist b/share/check_mk/checkman/unifi_device_shortlist new file mode 100644 index 0000000..5a3554c --- /dev/null +++ b/share/check_mk/checkman/unifi_device_shortlist @@ -0,0 +1,13 @@ +title: Unifi Devicelist +catalog: networking +agents: unifi_controller +licence: MIT +description: + plz fill me + +item: + The name of the device + +inventory: + One Service for each device + \ No newline at end of file diff --git a/share/check_mk/checkman/unifi_network_ports_if b/share/check_mk/checkman/unifi_network_ports_if new file mode 100644 index 0000000..ce4eae5 --- /dev/null +++ b/share/check_mk/checkman/unifi_network_ports_if @@ -0,0 +1,13 @@ +title: Unifi Network Port +agents: unifi_controller +catalog: networking +licence: MIT +description: + plz fill me + +item: + The name of the Port + +inventory: + One Service for each Port + \ No newline at end of file diff --git a/share/check_mk/checkman/unifi_network_radios b/share/check_mk/checkman/unifi_network_radios new file mode 100644 index 0000000..7e5a223 --- /dev/null +++ b/share/check_mk/checkman/unifi_network_radios @@ -0,0 +1,13 @@ +title: Unifi WLAN Radio +agents: unifi_controller +catalog: networking +licence: MIT +description: + plz fill me + +item: + The name of the Radio + +inventory: + One Service for each Radio + \ No newline at end of file diff --git a/share/check_mk/checkman/unifi_network_ssids b/share/check_mk/checkman/unifi_network_ssids new file mode 100644 index 0000000..3dc339e --- /dev/null +++ b/share/check_mk/checkman/unifi_network_ssids @@ -0,0 +1,13 @@ +title: Unifi SSID +agents: unifi_controller +catalog: networking +licence: MIT +description: + plz fill me + +item: + The name of the SSID + +inventory: + One Service for each SSID + \ No newline at end of file diff --git a/share/check_mk/checkman/unifi_sites b/share/check_mk/checkman/unifi_sites new file mode 100644 index 0000000..7295cdf --- /dev/null +++ b/share/check_mk/checkman/unifi_sites @@ -0,0 +1,13 @@ +title: Unifi Site +agents: unifi_controller +catalog: networking +licence: MIT +description: + plz fill me + +item: + The name of the Site + +inventory: + One Service for each Site + \ No newline at end of file diff --git a/share/check_mk/checkman/unifi_ssid_list b/share/check_mk/checkman/unifi_ssid_list new file mode 100644 index 0000000..5a3554c --- /dev/null +++ b/share/check_mk/checkman/unifi_ssid_list @@ -0,0 +1,13 @@ +title: Unifi Devicelist +catalog: networking +agents: unifi_controller +licence: MIT +description: + plz fill me + +item: + The name of the device + +inventory: + One Service for each device + \ No newline at end of file diff --git a/share/check_mk/checks/agent_unifi_controller b/share/check_mk/checks/agent_unifi_controller index 475b172..4e3b28d 100644 --- a/share/check_mk/checks/agent_unifi_controller +++ b/share/check_mk/checks/agent_unifi_controller @@ -1,5 +1,26 @@ #!/usr/bin/env python3 # -*- encoding: utf-8; py-indent-offset: 4 -*- +## MIT License +## +## Copyright (c) 2021 Bash Club +## +## Permission is hereby granted, free of charge, to any person obtaining a copy +## of this software and associated documentation files (the "Software"), to deal +## in the Software without restriction, including without limitation the rights +## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +## copies of the Software, and to permit persons to whom the Software is +## furnished to do so, subject to the following conditions: +## +## The above copyright notice and this permission notice shall be included in all +## copies or substantial portions of the Software. +## +## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +## SOFTWARE. #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 diff --git a/share/check_mk/web/plugins/metrics/unifi_metrics.py b/share/check_mk/web/plugins/metrics/unifi_metrics.py index c29dc2a..ae4d232 100644 --- a/share/check_mk/web/plugins/metrics/unifi_metrics.py +++ b/share/check_mk/web/plugins/metrics/unifi_metrics.py @@ -1,3 +1,27 @@ +#!/usr/bin/env python3 +# -*- encoding: utf-8; py-indent-offset: 4 -*- +## MIT License +## +## Copyright (c) 2021 Bash Club +## +## Permission is hereby granted, free of charge, to any person obtaining a copy +## of this software and associated documentation files (the "Software"), to deal +## in the Software without restriction, including without limitation the rights +## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +## copies of the Software, and to permit persons to whom the Software is +## furnished to do so, subject to the following conditions: +## +## The above copyright notice and this permission notice shall be included in all +## copies or substantial portions of the Software. +## +## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +## SOFTWARE. + from cmk.gui.i18n import _ from cmk.gui.plugins.metrics import ( metric_info, diff --git a/share/check_mk/web/plugins/perfometer/unifi_performeter.py b/share/check_mk/web/plugins/perfometer/unifi_performeter.py index 1c554c3..95b9864 100644 --- a/share/check_mk/web/plugins/perfometer/unifi_performeter.py +++ b/share/check_mk/web/plugins/perfometer/unifi_performeter.py @@ -1,3 +1,27 @@ +# -*- encoding: utf-8; py-indent-offset: 4 -*- + +## MIT License +## +## Copyright (c) 2021 Bash Club +## +## Permission is hereby granted, free of charge, to any person obtaining a copy +## of this software and associated documentation files (the "Software"), to deal +## in the Software without restriction, including without limitation the rights +## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +## copies of the Software, and to permit persons to whom the Software is +## furnished to do so, subject to the following conditions: +## +## The above copyright notice and this permission notice shall be included in all +## copies or substantial portions of the Software. +## +## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +## SOFTWARE. + from cmk.gui.plugins.metrics import perfometer_info perfometer_info.append({ diff --git a/share/check_mk/web/plugins/wato/datasource_unifi_controller.py b/share/check_mk/web/plugins/wato/datasource_unifi_controller.py index 2cf2457..3e5000b 100644 --- a/share/check_mk/web/plugins/wato/datasource_unifi_controller.py +++ b/share/check_mk/web/plugins/wato/datasource_unifi_controller.py @@ -1,5 +1,26 @@ #!/usr/bin/env python3 # -*- encoding: utf-8; py-indent-offset: 4 -*- +## MIT License +## +## Copyright (c) 2021 Bash Club +## +## Permission is hereby granted, free of charge, to any person obtaining a copy +## of this software and associated documentation files (the "Software"), to deal +## in the Software without restriction, including without limitation the rights +## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +## copies of the Software, and to permit persons to whom the Software is +## furnished to do so, subject to the following conditions: +## +## The above copyright notice and this permission notice shall be included in all +## copies or substantial portions of the Software. +## +## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +## SOFTWARE. from cmk.gui.i18n import _ from cmk.gui.plugins.wato import ( @@ -50,3 +71,4 @@ rulespec_registry.register( name='special_agents:unifi_controller', valuespec=_valuespec_special_agent_unifi_controller, )) +