From 16261bf827b7d05a99104788efee7bb7f1f226c1 Mon Sep 17 00:00:00 2001 From: Arseniy Kuznetsov Date: Sat, 9 Jan 2021 12:02:51 +0100 Subject: [PATCH] cli options, fixes --- README.md | 2 +- mktxp/basep.py | 3 +- mktxp/cli/config/_mktxp.conf | 4 ++ mktxp/cli/config/config.py | 82 +++++++++++++++++-------- mktxp/cli/config/mktxp.conf | 27 ++++---- mktxp/cli/dispatch.py | 6 +- mktxp/cli/options.py | 6 +- mktxp/collectors/capsman_collector.py | 34 ++++------ mktxp/collectors/dhcp_collector.py | 2 +- mktxp/collectors/health_collector.py | 2 +- mktxp/collectors/identity_collector.py | 2 +- mktxp/collectors/interface_collector.py | 6 +- mktxp/collectors/monitor_collector.py | 6 +- mktxp/collectors/pool_collector.py | 2 +- mktxp/collectors/resource_collector.py | 2 +- mktxp/collectors/route_collector.py | 2 +- mktxp/collectors/wlan_collector.py | 62 +++++++++++++++++-- mktxp/collectors_handler.py | 6 ++ mktxp/router_connection.py | 67 ++++++++++++++++---- mktxp/router_metric.py | 13 +++- setup.py | 2 +- 21 files changed, 239 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 45aaba1..9d8bb9b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![License](https://img.shields.io/badge/prometheus-exporter-blueviolet) - + diff --git a/mktxp/basep.py b/mktxp/basep.py index e26fb3a..6a81540 100644 --- a/mktxp/basep.py +++ b/mktxp/basep.py @@ -28,8 +28,7 @@ class MKTXPProcessor: def start(): router_metrics_handler = RouterMetricsHandler() REGISTRY.register(CollectorsHandler(router_metrics_handler)) - port=config_handler.mktxp_port() - MKTXPProcessor.run(port=port) + MKTXPProcessor.run(port=config_handler._entry().port) @staticmethod def run(server_class=HTTPServer, handler_class=MetricsHandler, port = None): diff --git a/mktxp/cli/config/_mktxp.conf b/mktxp/cli/config/_mktxp.conf index 41ddb6c..c793314 100644 --- a/mktxp/cli/config/_mktxp.conf +++ b/mktxp/cli/config/_mktxp.conf @@ -13,3 +13,7 @@ [MKTXP] port = 49090 + socket_timeout = 2 + initial_delay_on_failure = 120 + max_delay_on_failure = 900 + delay_inc_div = 5 diff --git a/mktxp/cli/config/config.py b/mktxp/cli/config/config.py index 90d5f45..f5237f2 100755 --- a/mktxp/cli/config/config.py +++ b/mktxp/cli/config/config.py @@ -33,7 +33,8 @@ class MKTXPConfigKeys: PASSWD_KEY = 'password' SSL_KEY = 'use_ssl' - SSL_CERTIFICATE = 'ssl_certificate' + NO_SSL_CERTIFICATE = 'no_ssl_certificate' + SSL_CERTIFICATE_VERIFY = 'ssl_certificate_verify' FE_DHCP_KEY = 'dhcp' FE_DHCP_LEASE_KEY = 'dhcp_lease' @@ -46,6 +47,12 @@ class MKTXPConfigKeys: FE_CAPSMAN_KEY = 'capsman' FE_CAPSMAN_CLIENTS_KEY = 'capsman_clients' + MKTXP_SOCKET_TIMEOUT = 'socket_timeout' + MKTXP_SOCKET_TIMEOUT = 'socket_timeout' + MKTXP_INITIAL_DELAY = 'initial_delay_on_failure' + MKTXP_MAX_DELAY = 'max_delay_on_failure' + MKTXP_INC_DIV = 'delay_inc_div' + # UnRegistered entries placeholder NO_ENTRIES_REGISTERED = 'NoEntriesRegistered' @@ -53,15 +60,21 @@ class MKTXPConfigKeys: ROUTERBOARD_NAME = 'routerboard_name' ROUTERBOARD_ADDRESS = 'routerboard_address' - # Default ports + # Default values DEFAULT_API_PORT = 8728 DEFAULT_API_SSL_PORT = 8729 - DEFAULT_MKTXP_PORT = 49090 + DEFAULT_MKTXP_PORT = 49090 + DEFAULT_MKTXP_SOCKET_TIMEOUT = 2 + DEFAULT_MKTXP_INITIAL_DELAY = 120 + DEFAULT_MKTXP_MAX_DELAY = 900 + DEFAULT_MKTXP_INC_DIV = 5 - BOOLEAN_KEYS = [ENABLED_KEY, SSL_KEY, SSL_CERTIFICATE, + BOOLEAN_KEYS = (ENABLED_KEY, SSL_KEY, NO_SSL_CERTIFICATE, SSL_CERTIFICATE_VERIFY, FE_DHCP_KEY, FE_DHCP_LEASE_KEY, FE_DHCP_POOL_KEY, FE_INTERFACE_KEY, - FE_MONITOR_KEY, FE_ROUTE_KEY, FE_WIRELESS_KEY, FE_WIRELESS_CLIENTS_KEY, FE_CAPSMAN_KEY, FE_CAPSMAN_CLIENTS_KEY] + FE_MONITOR_KEY, FE_ROUTE_KEY, FE_WIRELESS_KEY, FE_WIRELESS_CLIENTS_KEY, FE_CAPSMAN_KEY, FE_CAPSMAN_CLIENTS_KEY) + STR_KEYS = [HOST_KEY, USER_KEY, PASSWD_KEY] + INT_KEYS = [PORT_KEY, MKTXP_SOCKET_TIMEOUT, MKTXP_INITIAL_DELAY, MKTXP_MAX_DELAY, MKTXP_INC_DIV] # MKTXP config entry nane MKTXP_CONFIG_ENTRY_NAME = 'MKTXP' @@ -70,12 +83,15 @@ class MKTXPConfigKeys: class ConfigEntry: MKTXPEntry = namedtuple('MKTXPEntry', [MKTXPConfigKeys.ENABLED_KEY, MKTXPConfigKeys.HOST_KEY, MKTXPConfigKeys.PORT_KEY, MKTXPConfigKeys.USER_KEY, MKTXPConfigKeys.PASSWD_KEY, - MKTXPConfigKeys.SSL_KEY, MKTXPConfigKeys.SSL_CERTIFICATE, + MKTXPConfigKeys.SSL_KEY, MKTXPConfigKeys.NO_SSL_CERTIFICATE, MKTXPConfigKeys.SSL_CERTIFICATE_VERIFY, MKTXPConfigKeys.FE_DHCP_KEY, MKTXPConfigKeys.FE_DHCP_LEASE_KEY, MKTXPConfigKeys.FE_DHCP_POOL_KEY, MKTXPConfigKeys.FE_INTERFACE_KEY, MKTXPConfigKeys.FE_MONITOR_KEY, MKTXPConfigKeys.FE_ROUTE_KEY, MKTXPConfigKeys.FE_WIRELESS_KEY, MKTXPConfigKeys.FE_WIRELESS_CLIENTS_KEY, MKTXPConfigKeys.FE_CAPSMAN_KEY, MKTXPConfigKeys.FE_CAPSMAN_CLIENTS_KEY ]) + _MKTXPEntry = namedtuple('_MKTXPEntry', [MKTXPConfigKeys.PORT_KEY, MKTXPConfigKeys.MKTXP_SOCKET_TIMEOUT, + MKTXPConfigKeys.MKTXP_INITIAL_DELAY, MKTXPConfigKeys.MKTXP_MAX_DELAY, MKTXPConfigKeys.MKTXP_INC_DIV]) + class OSConfig(metaclass = ABCMeta): ''' OS-related config @@ -139,7 +155,7 @@ class MKTXPConfigHandler: ''' (Force-)Read conf data from disk ''' self.config = ConfigObj(self.usr_conf_data_path) - self.mktxp_config = ConfigObj(self.mktxp_conf_path) + self._config = ConfigObj(self.mktxp_conf_path) def _create_os_path(self, os_path, resource_path): if not os.path.exists(os_path): @@ -190,37 +206,55 @@ class MKTXPConfigHandler: def entry(self, entry_name): ''' Given an entry name, reads and returns the entry info ''' - entry_reader = self._entry_reader(entry_name) + entry_reader = self.entry_reader(entry_name) return ConfigEntry.MKTXPEntry(**entry_reader) - def mktxp_port(self): - if self.mktxp_config.get(MKTXPConfigKeys.MKTXP_CONFIG_ENTRY_NAME) and \ - self.mktxp_config[MKTXPConfigKeys.MKTXP_CONFIG_ENTRY_NAME].get(MKTXPConfigKeys.PORT_KEY): - return self.mktxp_config[MKTXPConfigKeys.MKTXP_CONFIG_ENTRY_NAME].as_int(MKTXPConfigKeys.PORT_KEY) - return MKTXPConfigKeys.DEFAULT_MKTXP_PORT + def _entry(self): + ''' MKTXP internal config entry + ''' + _entry_reader = self._entry_reader() + return ConfigEntry._MKTXPEntry(**_entry_reader) + # Helpers - def _entry_reader(self, entry_name): - entry = {} + def entry_reader(self, entry_name): + entry_reader = {} for key in MKTXPConfigKeys.BOOLEAN_KEYS: if self.config[entry_name].get(key): - entry[key] = self.config[entry_name].as_bool(key) + entry_reader[key] = self.config[entry_name].as_bool(key) else: - entry[key] = False + entry_reader[key] = False for key in MKTXPConfigKeys.STR_KEYS: - entry[key] = self.config[entry_name][key] + entry_reader[key] = self.config[entry_name][key] # port if self.config[entry_name].get(MKTXPConfigKeys.PORT_KEY): - entry[MKTXPConfigKeys.PORT_KEY] = self.config[entry_name].as_int(MKTXPConfigKeys.PORT_KEY) + entry_reader[MKTXPConfigKeys.PORT_KEY] = self.config[entry_name].as_int(MKTXPConfigKeys.PORT_KEY) else: - if entry[MKTXPConfigKeys.SSL_KEY]: - entry[MKTXPConfigKeys.PORT_KEY] = MKTXPConfigKeys.DEFAULT_API_SSL_PORT - else: - entry[MKTXPConfigKeys.PORT_KEY] = MKTXPConfigKeys.DEFAULT_API_PORT + entry_reader[MKTXPConfigKeys.PORT_KEY] = self._default_value_for_key(MKTXPConfigKeys.SSL_KEY, entry_reader[MKTXPConfigKeys.SSL_KEY]) - return entry + return entry_reader + + def _entry_reader(self): + _entry_reader = {} + entry_name = MKTXPConfigKeys.MKTXP_CONFIG_ENTRY_NAME + for key in MKTXPConfigKeys.INT_KEYS: + if self._config[entry_name].get(key): + _entry_reader[key] = self._config[entry_name].as_int(key) + else: + _entry_reader[key] = self._default_value_for_key(key) + return _entry_reader + + def _default_value_for_key(self, key, value = None): + return { + MKTXPConfigKeys.SSL_KEY: lambda value: MKTXPConfigKeys.DEFAULT_API_SSL_PORT if value else MKTXPConfigKeys.DEFAULT_API_PORT, + MKTXPConfigKeys.PORT_KEY: lambda value: MKTXPConfigKeys.DEFAULT_MKTXP_PORT, + MKTXPConfigKeys.MKTXP_SOCKET_TIMEOUT: lambda value: MKTXPConfigKeys.DEFAULT_MKTXP_SOCKET_TIMEOUT, + MKTXPConfigKeys.MKTXP_INITIAL_DELAY: lambda value: MKTXPConfigKeys.DEFAULT_MKTXP_INITIAL_DELAY, + MKTXPConfigKeys.MKTXP_MAX_DELAY: lambda value: MKTXPConfigKeys.DEFAULT_MKTXP_MAX_DELAY, + MKTXPConfigKeys.MKTXP_INC_DIV: lambda value: MKTXPConfigKeys.DEFAULT_MKTXP_INC_DIV + }[key](value) # Simplest possible Singleton impl diff --git a/mktxp/cli/config/mktxp.conf b/mktxp/cli/config/mktxp.conf index 664934b..4b01753 100644 --- a/mktxp/cli/config/mktxp.conf +++ b/mktxp/cli/config/mktxp.conf @@ -11,18 +11,19 @@ ## GNU General Public License for more details. -[SampleRouter] - enabled = False - - hostname = localhost - port = 8728 - - username = user +[Sample-Router] + enabled = False # turns metrics collection for this RouterOS device on / off + + hostname = localhost # RouterOS IP address + port = 8728 # RouterOS IP Port + + username = username # RouterOS user, needs to have 'read' and 'api' permissions password = password - - use_ssl = False - ssl_certificate = False - + + use_ssl = False # enables connection via API-SSL servis + no_ssl_certificate = False # enables API_SSL connect without router SSL certificate + ssl_certificate_verify = False # turns SSL certificate verification on / off + dhcp = True dhcp_lease = True pool = True @@ -32,4 +33,6 @@ wireless = True wireless_clients = True capsman = True - capsman_clients = True \ No newline at end of file + capsman_clients = True + + \ No newline at end of file diff --git a/mktxp/cli/dispatch.py b/mktxp/cli/dispatch.py index 6b7a7ef..ece856a 100755 --- a/mktxp/cli/dispatch.py +++ b/mktxp/cli/dispatch.py @@ -72,8 +72,10 @@ class MKTXPDispatcher: def show_entries(self, args): - if args['configpath']: - print(f'MKTX config path: {config_handler.usr_conf_data_path}') + if args['config']: + print(f'MKTXP data config: {config_handler.usr_conf_data_path}') + print(f'MKTXP internal config: {config_handler.mktxp_conf_path}') + else: for entryname in config_handler.registered_entries(): if args['entry_name'] and entryname != args['entry_name']: diff --git a/mktxp/cli/options.py b/mktxp/cli/options.py index a1de267..c4cd092 100755 --- a/mktxp/cli/options.py +++ b/mktxp/cli/options.py @@ -101,8 +101,8 @@ class MKTXPOptionsParser: description = 'Displays MKTXP config router entries', formatter_class=MKTXPHelpFormatter) self._add_entry_name(show_parser, registered_only = True, required = False, help = "Config entry name") - show_parser.add_argument('-cp', '--configpath', dest='configpath', - help = "Shows MKTXP config file path", + show_parser.add_argument('-cfg', '--config', dest='config', + help = "Shows MKTXP config files paths", action = 'store_true') # Add command @@ -137,7 +137,7 @@ class MKTXPOptionsParser: optional_args_group.add_argument('-ssl', '--use-ssl', dest='use_ssl', help = "Connect via RouterOS api-ssl service", action = 'store_true') - optional_args_group.add_argument('-ssl-cert', '--use-ssl-certificate', dest='ssl_certificate', + optional_args_group.add_argument('-no-ssl-cert', '--no-ssl-certificate', dest='no_ssl_certificate', help = "Connect with configured RouterOS SSL ceritficate", action = 'store_true') diff --git a/mktxp/collectors/capsman_collector.py b/mktxp/collectors/capsman_collector.py index bcdfab8..6c2b80f 100644 --- a/mktxp/collectors/capsman_collector.py +++ b/mktxp/collectors/capsman_collector.py @@ -25,7 +25,7 @@ class CapsmanCollector(BaseCollector): remote_caps_labels = ['identity', 'version', 'base_mac', 'board', 'base_mac'] remote_caps_records = router_metric.capsman_remote_caps_records(remote_caps_labels) if not remote_caps_records: - return + return range(0) remote_caps_metrics = BaseCollector.info_collector('capsman_remote_caps', 'CAPsMAN remote caps', remote_caps_records, remote_caps_labels) yield remote_caps_metrics @@ -33,7 +33,7 @@ class CapsmanCollector(BaseCollector): registration_labels = ['interface', 'ssid', 'mac_address', 'tx_rate', 'rx_rate', 'rx_signal', 'uptime', 'bytes'] registration_records = router_metric.capsman_registration_table_records(registration_labels) if not registration_records: - return + return range(0) # calculate number of registrations per interface registration_per_interface = {} @@ -53,40 +53,28 @@ class CapsmanCollector(BaseCollector): dhcp_lease_labels = ['mac_address', 'host_name', 'comment'] dhcp_lease_records = router_metric.dhcp_lease_records(dhcp_lease_labels) for registration_record in registration_records: - dhcp_lease_record = next((dhcp_lease_record for dhcp_lease_record in dhcp_lease_records if dhcp_lease_record['mac_address']==registration_record['mac_address'])) - if dhcp_lease_record: - registration_record['name'] = dhcp_lease_record.get('comment', dhcp_lease_record.get('host_name', dhcp_lease_record.get('mac_address'))) - else: - registration_record['name'] = registration_record['mac_address'] + try: + dhcp_lease_record = next((dhcp_lease_record for dhcp_lease_record in dhcp_lease_records if dhcp_lease_record['mac_address']==registration_record['mac_address'])) + registration_record['name'] = dhcp_lease_record.get('comment', dhcp_lease_record.get('host_name', dhcp_lease_record.get('mac_address'))) + except StopIteration: + registration_record['name'] = f"{registration_record['mac_address']}: No DHCP registration" # split out tx/rx bytes registration_record['tx_bytes'] = registration_record['bytes'].split(',')[0] registration_record['rx_bytes'] = registration_record['bytes'].split(',')[1] del registration_record['bytes'] - tx_byte_metrics = BaseCollector.counter_collector('capsman_traffic_tx_bytes', 'Number of sent packet bytes', registration_records, 'tx_bytes', ['name']) + tx_byte_metrics = BaseCollector.counter_collector('capsman_clients_tx_bytes', 'Number of sent packet bytes', registration_records, 'tx_bytes', ['name']) yield tx_byte_metrics - rx_byte_metrics = BaseCollector.counter_collector('capsman_traffic_rx_bytes', 'Number of received packet bytes', registration_records, 'rx_bytes', ['name']) + rx_byte_metrics = BaseCollector.counter_collector('capsman_clients_rx_bytes', 'Number of received packet bytes', registration_records, 'rx_bytes', ['name']) yield rx_byte_metrics - signal_strength_metrics = BaseCollector.gauge_collector('capsman_registered_signal_strength', 'Registered devices signal strength', registration_records, 'rx_signal', ['name']) + signal_strength_metrics = BaseCollector.gauge_collector('capsman_clients_signal_strength', 'Client devices signal strength', registration_records, 'rx_signal', ['name']) yield signal_strength_metrics - registration_metrics = BaseCollector.info_collector('capsman_registered_devices', 'Registered devices info', + registration_metrics = BaseCollector.info_collector('capsman_clients_devices', 'Registered client devices info', registration_records, ['name', 'rx_signal', 'ssid', 'tx_rate', 'rx_rate', 'interface', 'mac_address', 'uptime']) yield registration_metrics - - - - - - - - - - - - diff --git a/mktxp/collectors/dhcp_collector.py b/mktxp/collectors/dhcp_collector.py index f0807f0..4db6949 100644 --- a/mktxp/collectors/dhcp_collector.py +++ b/mktxp/collectors/dhcp_collector.py @@ -24,7 +24,7 @@ class DHCPCollector(BaseCollector): dhcp_lease_labels = ['active_address', 'mac_address', 'host_name', 'comment', 'server', 'expires_after'] dhcp_lease_records = router_metric.dhcp_lease_records(dhcp_lease_labels) if not dhcp_lease_records: - return + return range(0) # calculate number of leases per DHCP server dhcp_lease_servers = {} diff --git a/mktxp/collectors/health_collector.py b/mktxp/collectors/health_collector.py index 7cacf0f..f596bd4 100644 --- a/mktxp/collectors/health_collector.py +++ b/mktxp/collectors/health_collector.py @@ -23,7 +23,7 @@ class HealthCollector(BaseCollector): health_labels = ['voltage', 'temperature'] health_records = router_metric.health_records(health_labels) if not health_records: - return + return range(0) voltage_metrics = BaseCollector.gauge_collector('system_routerboard_voltage', 'Supplied routerboard voltage', health_records, 'voltage') yield voltage_metrics diff --git a/mktxp/collectors/identity_collector.py b/mktxp/collectors/identity_collector.py index f838cce..6814aa5 100644 --- a/mktxp/collectors/identity_collector.py +++ b/mktxp/collectors/identity_collector.py @@ -23,7 +23,7 @@ class IdentityCollector(BaseCollector): identity_labels = ['name'] identity_records = router_metric.identity_records(identity_labels) if not identity_records: - return + return range(0) identity_metrics = BaseCollector.info_collector('system_identity', 'System identity', identity_records, identity_labels) yield identity_metrics diff --git a/mktxp/collectors/interface_collector.py b/mktxp/collectors/interface_collector.py index 2403565..b752071 100644 --- a/mktxp/collectors/interface_collector.py +++ b/mktxp/collectors/interface_collector.py @@ -23,7 +23,11 @@ class InterfaceCollector(BaseCollector): interface_traffic_labels = ['name', 'comment', 'rx_byte', 'tx_byte', 'rx_packet', 'tx_packet', 'rx_error', 'tx_error', 'rx_drop', 'tx_drop'] interface_traffic_records = router_metric.interface_traffic_records(interface_traffic_labels) if not interface_traffic_records: - return + return range(0) + + for interface_traffic_record in interface_traffic_records: + if interface_traffic_record.get('comment'): + interface_traffic_record['name'] = f"{interface_traffic_record['name']} ({interface_traffic_record['comment']})" rx_byte_metric = BaseCollector.counter_collector('interface_rx_byte', 'Number of received bytes', interface_traffic_records, 'rx_byte', ['name']) yield rx_byte_metric diff --git a/mktxp/collectors/monitor_collector.py b/mktxp/collectors/monitor_collector.py index 3b97863..b37a406 100644 --- a/mktxp/collectors/monitor_collector.py +++ b/mktxp/collectors/monitor_collector.py @@ -21,10 +21,10 @@ class MonitorCollector(BaseCollector): ''' @staticmethod def collect(router_metric): - monitor_labels = ['status', 'rate', 'full_duplex', 'name'] - monitor_records = router_metric.interface_monitor_records(monitor_labels) + monitor_labels = ('status', 'rate', 'full_duplex', 'name') + monitor_records = router_metric.interface_monitor_records(monitor_labels, include_comments = True) if not monitor_records: - return + return range(0) # translate records to appropriate values for monitor_record in monitor_records: diff --git a/mktxp/collectors/pool_collector.py b/mktxp/collectors/pool_collector.py index 7d6a093..dfa12b9 100644 --- a/mktxp/collectors/pool_collector.py +++ b/mktxp/collectors/pool_collector.py @@ -25,7 +25,7 @@ class PoolCollector(BaseCollector): # initialize all pool counts, including those currently not used pool_records = router_metric.pool_records(['name']) if not pool_records: - return + return range(0) pool_used_labels = ['pool'] pool_used_counts = {pool_record['name']: 0 for pool_record in pool_records} diff --git a/mktxp/collectors/resource_collector.py b/mktxp/collectors/resource_collector.py index 10efc90..5b18ecf 100644 --- a/mktxp/collectors/resource_collector.py +++ b/mktxp/collectors/resource_collector.py @@ -28,7 +28,7 @@ class SystemResourceCollector(BaseCollector): 'architecture_name', 'board_name'] resource_records = router_metric.system_resource_records(resource_labels) if not resource_records: - return + return range(0) # translate records to appropriate values translated_fields = ['uptime'] diff --git a/mktxp/collectors/route_collector.py b/mktxp/collectors/route_collector.py index 88643fd..53ff40d 100644 --- a/mktxp/collectors/route_collector.py +++ b/mktxp/collectors/route_collector.py @@ -24,7 +24,7 @@ class RouteCollector(BaseCollector): route_labels = ['connect', 'dynamic', 'static', 'bgp', 'ospf'] route_records = router_metric.route_records(route_labels) if not route_records: - return + return range(0) # compile total routes records total_routes = len(route_records) diff --git a/mktxp/collectors/wlan_collector.py b/mktxp/collectors/wlan_collector.py index 615f049..1ba5dc4 100644 --- a/mktxp/collectors/wlan_collector.py +++ b/mktxp/collectors/wlan_collector.py @@ -11,6 +11,7 @@ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. +import re from mktxp.collectors.base_collector import BaseCollector from mktxp.router_metric import RouterMetric @@ -20,25 +21,76 @@ class WLANCollector(BaseCollector): ''' @staticmethod def collect(router_metric): - monitor_labels = ['channel', 'noise_floor', 'overall_tx_ccq'] + monitor_labels = ['channel', 'noise_floor', 'overall_tx_ccq', 'registered_clients'] monitor_records = router_metric.interface_monitor_records(monitor_labels, 'wireless') if not monitor_records: - return + return range(0) # sanitize records for relevant labels noise_floor_records = [monitor_record for monitor_record in monitor_records if monitor_record.get('noise_floor')] tx_ccq_records = [monitor_record for monitor_record in monitor_records if monitor_record.get('overall_tx_ccq')] + registered_clients_records = [monitor_record for monitor_record in monitor_records if monitor_record.get('registered_clients')] if noise_floor_records: noise_floor_metrics = BaseCollector.gauge_collector('wlan_noise_floor', 'Noise floor threshold', noise_floor_records, 'noise_floor', ['channel']) yield noise_floor_metrics if tx_ccq_records: - overall_tx_ccq_metrics = BaseCollector.gauge_collector('wlan_overall_tx_ccq', ' Client Connection Quality for transmitting', tx_ccq_records, 'overall_tx_ccq', ['channel']) + overall_tx_ccq_metrics = BaseCollector.gauge_collector('wlan_overall_tx_ccq', 'Client Connection Quality for transmitting', tx_ccq_records, 'overall_tx_ccq', ['channel']) yield overall_tx_ccq_metrics + if registered_clients_records: + registered_clients_metrics = BaseCollector.gauge_collector('wlan_registered_clients', 'Number of registered clients', registered_clients_records, 'registered_clients', ['channel']) + yield registered_clients_metrics + # the client info metrics if router_metric.router_entry.wireless_clients: - # TBD - pass \ No newline at end of file + registration_labels = ['interface', 'ssid', 'mac_address', 'tx_rate', 'rx_rate', 'uptime', 'bytes', 'signal_to_noise', 'tx_ccq', 'signal_strength'] + registration_records = router_metric.wireless_registration_table_records(registration_labels) + if not registration_records: + return range(0) + + dhcp_lease_labels = ['mac_address', 'host_name', 'comment'] + dhcp_lease_records = router_metric.dhcp_lease_records(dhcp_lease_labels) + + for registration_record in registration_records: + try: + dhcp_lease_record = next((dhcp_lease_record for dhcp_lease_record in dhcp_lease_records if dhcp_lease_record['mac_address']==registration_record['mac_address'])) + registration_record['name'] = dhcp_lease_record.get('comment', dhcp_lease_record.get('host_name', dhcp_lease_record.get('mac_address'))) + except StopIteration: + registration_record['name'] = registration_record['mac_address'] + + # split out tx/rx bytes + registration_record['tx_bytes'] = registration_record['bytes'].split(',')[0] + registration_record['rx_bytes'] = registration_record['bytes'].split(',')[1] + + # average signal strength + registration_record['signal_strength'] = re.search(r'-\d+', registration_record['signal_strength']).group() + + del registration_record['bytes'] + + tx_byte_metrics = BaseCollector.counter_collector('wlan_clients_tx_bytes', 'Number of sent packet bytes', registration_records, 'tx_bytes', ['name']) + yield tx_byte_metrics + + rx_byte_metrics = BaseCollector.counter_collector('wlan_clients_rx_bytes', 'Number of received packet bytes', registration_records, 'rx_bytes', ['name']) + yield rx_byte_metrics + + signal_strength_metrics = BaseCollector.gauge_collector('wlan_clients_signal_strength', 'Average strength of the client signal recevied by AP', registration_records, 'signal_strength', ['name']) + yield signal_strength_metrics + + signal_to_noise_metrics = BaseCollector.gauge_collector('wlan_clients_signal_to_noise', 'Client devices signal to noise ratio', registration_records, 'signal_to_noise', ['name']) + yield signal_to_noise_metrics + + tx_ccq_metrics = BaseCollector.gauge_collector('wlan_clients_tx_ccq', 'Client Connection Quality (CCQ) for transmit', registration_records, 'tx_ccq', ['name']) + yield tx_ccq_metrics + + registration_metrics = BaseCollector.info_collector('wlan_clients_devices', 'Client devices info', + registration_records, ['name', 'rx_signal', 'ssid', 'tx_rate', 'rx_rate', 'interface', 'mac_address', 'uptime']) + yield registration_metrics + + + return range(0) + + + diff --git a/mktxp/collectors_handler.py b/mktxp/collectors_handler.py index e5f5858..a0d3af5 100644 --- a/mktxp/collectors_handler.py +++ b/mktxp/collectors_handler.py @@ -30,6 +30,11 @@ class CollectorsHandler: def collect(self): for router_metric in self.metrics_handler.router_metrics: + if not router_metric.api_connection.is_connected(): + # let's pick up on things in the next run + router_metric.api_connection.connect() + continue + yield from IdentityCollector.collect(router_metric) yield from SystemResourceCollector.collect(router_metric) yield from HealthCollector.collect(router_metric) @@ -55,3 +60,4 @@ class CollectorsHandler: if router_metric.router_entry.capsman: yield from CapsmanCollector.collect(router_metric) + return range(0) diff --git a/mktxp/router_connection.py b/mktxp/router_connection.py index fc2fd93..c6251fc 100644 --- a/mktxp/router_connection.py +++ b/mktxp/router_connection.py @@ -15,7 +15,7 @@ import ssl import socket from datetime import datetime from routeros_api import RouterOsApiPool - +from mktxp.cli.config.config import config_handler class RouterAPIConnectionError(Exception): pass @@ -26,10 +26,11 @@ class RouterAPIConnection: ''' def __init__(self, router_name, router_entry): self.router_name = router_name - self.router_entry = router_entry + self.router_entry = router_entry + self.last_failure_timestamp = self.successive_failure_count = 0 ctx = None - if self.router_entry.use_ssl and not self.router_entry.ssl_certificate: + if self.router_entry.use_ssl and self.router_entry.no_ssl_certificate: ctx = ssl.create_default_context() ctx.set_ciphers('ADH:@SECLEVEL=0') @@ -40,9 +41,10 @@ class RouterAPIConnection: port = self.router_entry.port, plaintext_login = True, use_ssl = self.router_entry.use_ssl, + ssl_verify = self.router_entry.ssl_certificate_verify, ssl_context = ctx) - self.connection.socket_timeout = 2 + self.connection.socket_timeout = config_handler._entry().socket_timeout self.api = None def is_connected(self): @@ -51,24 +53,63 @@ class RouterAPIConnection: try: self.api.get_resource('/system/identity').get() return True - except (socket.error, socket.timeout, Exception) as ex: - print(f'Connection to router {self.router_name}@{self.router_entry.hostname} has been lost: {ex}') - self.api = None + except (socket.error, socket.timeout, Exception) as exc: + self._set_connect_state(success = False, exc = exc) return False def connect(self): - if self.is_connected(): + connect_time = datetime.now() + if self.is_connected() or self._in_connect_timeout(connect_time.timestamp()): return - current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") try: print(f'Connecting to router {self.router_name}@{self.router_entry.hostname}') self.api = self.connection.get_api() - print(f'{current_time} Connection to router {self.router_name}@{self.router_entry.hostname} has been established') - except (socket.error, socket.timeout, Exception) as ex: - print(f'{current_time} Connection to router {self.router_name}@{self.router_entry.hostname} has failed: {ex}') - raise + self._set_connect_state(success = True, connect_time = connect_time) + except (socket.error, socket.timeout, Exception) as exc: + self._set_connect_state(success = False, connect_time = connect_time, exc = exc) + #raise RouterAPIConnectionError def router_api(self): if not self.is_connected(): self.connect() return self.api + + def _in_connect_timeout(self, connect_timestamp, quiet = True): + connect_delay = self._connect_delay() + if (connect_timestamp - self.last_failure_timestamp) < connect_delay: + if not quiet: + print(f'{self.router_name}@{self.router_entry.hostname}: in connect timeout, {int(connect_delay - (connect_timestamp - self.last_failure_timestamp))}secs remaining') + print(f'Successive failure count: {self.successive_failure_count}') + return True + if not quiet: + print(f'{self.router_name}@{self.router_entry.hostname}: OK to connect') + if self.last_failure_timestamp > 0: + print(f'Seconds since last failure: {connect_timestamp - self.last_failure_timestamp}') + print(f'Prior successive failure count: {self.successive_failure_count}') + return False + + def _connect_delay(self): + mktxp_entry = config_handler._entry() + connect_delay = (1 + self.successive_failure_count / mktxp_entry.delay_inc_div) * mktxp_entry.initial_delay_on_failure + return connect_delay if connect_delay < mktxp_entry.max_delay_on_failure else mktxp_entry.max_delay_on_failure + + + def _set_connect_state(self, success = False, connect_time = datetime.now(), exc = None): + if success: + self.last_failure_timestamp = 0 + self.successive_failure_count = 0 + print(f'{connect_time.strftime("%Y-%m-%d %H:%M:%S")} Connection to router {self.router_name}@{self.router_entry.hostname} has been established') + else: + self.api = None + self.successive_failure_count += 1 + self.last_failure_timestamp = connect_time.timestamp() + print(f'{connect_time.strftime("%Y-%m-%d %H:%M:%S")} Connection to router {self.router_name}@{self.router_entry.hostname} has failed: {exc}') + + + + + + + + + diff --git a/mktxp/router_metric.py b/mktxp/router_metric.py index 1ed01b9..91befb9 100644 --- a/mktxp/router_metric.py +++ b/mktxp/router_metric.py @@ -76,13 +76,20 @@ class RouterMetric: print(f'Error getting interface traffic info from router{self.router_name}@{self.router_entry.hostname}: {exc}') return None - def interface_monitor_records(self, interface_monitor_labels = [], kind = 'ethernet'): + def interface_monitor_records(self, interface_monitor_labels = [], kind = 'ethernet', include_comments = False): try: interfaces = self.api_connection.router_api().get_resource(f'/interface/{kind}').get() - interface_names = [interface['name'] for interface in interfaces] + interface_names = [(interface['name'], interface.get('comment')) for interface in interfaces] interface_monitor = lambda int_num : self.api_connection.router_api().get_resource(f'/interface/{kind}').call('monitor', {'once':'', 'numbers':f'{int_num}'}) interface_monitor_records = [interface_monitor(int_num)[0] for int_num in range(len(interface_names))] + + if include_comments: + # combine interfaces names with comments + for interface_monitor_record in interface_monitor_records: + for interface_name in interface_names: + if interface_name[1] and interface_name[0] == interface_monitor_record['name']: + interface_monitor_record['name'] = f"{interface_monitor_record['name']} ({interface_name[1]})" return self._trimmed_records(interface_monitor_records, interface_monitor_labels) except Exception as exc: print(f'Error getting {kind} interface monitor info from router{self.router_name}@{self.router_entry.hostname}: {exc}') @@ -109,7 +116,7 @@ class RouterMetric: route_records = self.api_connection.router_api().get_resource('/ip/route').get(active='yes') return self._trimmed_records(route_records, route_labels) except Exception as exc: - print(f'Error getting pool active routes info from router{self.router_name}@{self.router_entry.hostname}: {exc}') + print(f'Error getting routes info from router{self.router_name}@{self.router_entry.hostname}: {exc}') return None def wireless_registration_table_records(self, registration_table_labels = []): diff --git a/setup.py b/setup.py index 24b3037..71a7802 100755 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ with open(path.join(pkg_dir, 'README.md'), encoding='utf-8') as f: setup( name='mktxp', - version='0.18', + version='0.20', url='https://github.com/akpw/mktxp',