diff --git a/README.md b/README.md index 81187d1..57a652b 100644 --- a/README.md +++ b/README.md @@ -39,18 +39,19 @@ Commands: ## Full description of CLI Commands ### mktxp . action commands: - .. start Starts collecting metrics for all enabled RouterOS configuration entries + .. export Starts collecting metrics for all enabled RouterOS configuration entries + .. print Displays seleted metrics on the command line + .. info Shows base MKTXP info .. edit Open MKTXP configuration file in your editor of choice .. add Adds MKTXP RouterOS configuration entry from the command line .. show Shows MKTXP configuration entries on the command line .. delete Deletes a MKTXP RouterOS configuration entry from the command line - .. info Shows base MKTXP info - .. version Shows MKTXP version ## Installing Development version - Clone the repo, then run: `$ python setup.py develop` + **Running Tests** - TDB - Run via: `$ python setup.py test` diff --git a/mktxp/basep.py b/mktxp/basep.py index 6a81540..f67e160 100644 --- a/mktxp/basep.py +++ b/mktxp/basep.py @@ -11,7 +11,6 @@ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. - from http.server import HTTPServer from datetime import datetime from prometheus_client.core import REGISTRY @@ -20,6 +19,8 @@ from mktxp.cli.config.config import config_handler from mktxp.collectors_handler import CollectorsHandler from mktxp.metrics_handler import RouterMetricsHandler +from mktxp.cli.output.capsman_out import CapsmanOutput +from mktxp.cli.output.wifi_out import WirelessOutput class MKTXPProcessor: ''' Base Export Processing @@ -35,5 +36,22 @@ class MKTXPProcessor: server_address = ('', port) httpd = server_class(server_address, handler_class) current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - print(f'{current_time} Running metrics HTTP server on port {port}') + print(f'{current_time} Running HTTP metrics server on port {port}') httpd.serve_forever() + + +class MKTXPCLIProcessor: + ''' Base CLI Processing + ''' + @staticmethod + def capsman_clients(entry_name): + router_metric = RouterMetricsHandler.router_metric(entry_name) + if router_metric: + CapsmanOutput.clients_summary(router_metric) + + @staticmethod + def wifi_clients(entry_name): + router_metric = RouterMetricsHandler.router_metric(entry_name) + if router_metric: + WirelessOutput.clients_summary(router_metric) + diff --git a/mktxp/cli/config/_mktxp.conf b/mktxp/cli/config/_mktxp.conf index c793314..9888f25 100644 --- a/mktxp/cli/config/_mktxp.conf +++ b/mktxp/cli/config/_mktxp.conf @@ -17,3 +17,4 @@ initial_delay_on_failure = 120 max_delay_on_failure = 900 delay_inc_div = 5 + bandwidth_test_interval = 420 diff --git a/mktxp/cli/config/config.py b/mktxp/cli/config/config.py index f5237f2..5d5f8bd 100755 --- a/mktxp/cli/config/config.py +++ b/mktxp/cli/config/config.py @@ -52,10 +52,14 @@ class MKTXPConfigKeys: MKTXP_INITIAL_DELAY = 'initial_delay_on_failure' MKTXP_MAX_DELAY = 'max_delay_on_failure' MKTXP_INC_DIV = 'delay_inc_div' + MKTXP_BANDWIDTH_TEST_INTERVAL = 'bandwidth_test_interval' # UnRegistered entries placeholder NO_ENTRIES_REGISTERED = 'NoEntriesRegistered' + MKTXP_USE_COMMENTS_OVER_NAMES = 'use_comments_over_names' + + # Base router id labels ROUTERBOARD_NAME = 'routerboard_name' ROUTERBOARD_ADDRESS = 'routerboard_address' @@ -68,13 +72,15 @@ class MKTXPConfigKeys: DEFAULT_MKTXP_INITIAL_DELAY = 120 DEFAULT_MKTXP_MAX_DELAY = 900 DEFAULT_MKTXP_INC_DIV = 5 + DEFAULT_MKTXP_BANDWIDTH_TEST_INTERVAL = 420 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, MKTXP_USE_COMMENTS_OVER_NAMES, + 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] + STR_KEYS = (HOST_KEY, USER_KEY, PASSWD_KEY) + MKTXP_INT_KEYS = (PORT_KEY, MKTXP_SOCKET_TIMEOUT, MKTXP_INITIAL_DELAY, MKTXP_MAX_DELAY, MKTXP_INC_DIV, MKTXP_BANDWIDTH_TEST_INTERVAL) # MKTXP config entry nane MKTXP_CONFIG_ENTRY_NAME = 'MKTXP' @@ -87,10 +93,11 @@ class ConfigEntry: 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 + MKTXPConfigKeys.FE_CAPSMAN_KEY, MKTXPConfigKeys.FE_CAPSMAN_CLIENTS_KEY, MKTXPConfigKeys.MKTXP_USE_COMMENTS_OVER_NAMES ]) _MKTXPEntry = namedtuple('_MKTXPEntry', [MKTXPConfigKeys.PORT_KEY, MKTXPConfigKeys.MKTXP_SOCKET_TIMEOUT, - MKTXPConfigKeys.MKTXP_INITIAL_DELAY, MKTXPConfigKeys.MKTXP_MAX_DELAY, MKTXPConfigKeys.MKTXP_INC_DIV]) + MKTXPConfigKeys.MKTXP_INITIAL_DELAY, MKTXPConfigKeys.MKTXP_MAX_DELAY, + MKTXPConfigKeys.MKTXP_INC_DIV, MKTXPConfigKeys.MKTXP_BANDWIDTH_TEST_INTERVAL]) class OSConfig(metaclass = ABCMeta): @@ -149,6 +156,8 @@ class MKTXPConfigHandler: self._create_os_path(self.usr_conf_data_path, 'mktxp/cli/config/mktxp.conf') self._create_os_path(self.mktxp_conf_path, 'mktxp/cli/config/_mktxp.conf') + self.re_compiled = {} + self._read_from_disk() def _read_from_disk(self): @@ -219,11 +228,13 @@ class MKTXPConfigHandler: # Helpers def entry_reader(self, entry_name): entry_reader = {} + write_needed = False for key in MKTXPConfigKeys.BOOLEAN_KEYS: if self.config[entry_name].get(key): entry_reader[key] = self.config[entry_name].as_bool(key) else: entry_reader[key] = False + write_needed = True # read from disk next time for key in MKTXPConfigKeys.STR_KEYS: entry_reader[key] = self.config[entry_name][key] @@ -233,17 +244,29 @@ class MKTXPConfigHandler: entry_reader[MKTXPConfigKeys.PORT_KEY] = self.config[entry_name].as_int(MKTXPConfigKeys.PORT_KEY) else: entry_reader[MKTXPConfigKeys.PORT_KEY] = self._default_value_for_key(MKTXPConfigKeys.SSL_KEY, entry_reader[MKTXPConfigKeys.SSL_KEY]) + write_needed = True # read from disk next time + + if write_needed: + self.config[entry_name] = entry_reader + self.config.write() return entry_reader def _entry_reader(self): _entry_reader = {} entry_name = MKTXPConfigKeys.MKTXP_CONFIG_ENTRY_NAME - for key in MKTXPConfigKeys.INT_KEYS: + write_needed = False + for key in MKTXPConfigKeys.MKTXP_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) + write_needed = True # read from disk next time + + if write_needed: + self._config[entry_name] = _entry_reader + self._config.write() + return _entry_reader def _default_value_for_key(self, key, value = None): @@ -253,9 +276,13 @@ class MKTXPConfigHandler: 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 + MKTXPConfigKeys.MKTXP_INC_DIV: lambda value: MKTXPConfigKeys.DEFAULT_MKTXP_INC_DIV, + MKTXPConfigKeys.MKTXP_BANDWIDTH_TEST_INTERVAL: lambda value: MKTXPConfigKeys.DEFAULT_MKTXP_BANDWIDTH_TEST_INTERVAL }[key](value) # Simplest possible Singleton impl config_handler = MKTXPConfigHandler() + + + diff --git a/mktxp/cli/config/mktxp.conf b/mktxp/cli/config/mktxp.conf index 4b01753..21cef8e 100644 --- a/mktxp/cli/config/mktxp.conf +++ b/mktxp/cli/config/mktxp.conf @@ -22,8 +22,8 @@ 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 - + ssl_certificate_verify = False # turns SSL certificate verification on / off + dhcp = True dhcp_lease = True pool = True @@ -35,4 +35,4 @@ capsman = True capsman_clients = True - \ No newline at end of file + use_comments_over_names = False # when available, use comments instead of interface names diff --git a/mktxp/cli/dispatch.py b/mktxp/cli/dispatch.py index ece856a..05ce87a 100755 --- a/mktxp/cli/dispatch.py +++ b/mktxp/cli/dispatch.py @@ -14,12 +14,11 @@ import sys import subprocess -import pkg_resources import mktxp.cli.checks.chk_pv from mktxp.utils.utils import run_cmd from mktxp.cli.options import MKTXPOptionsParser, MKTXPCommands from mktxp.cli.config.config import config_handler, ConfigEntry -from mktxp.basep import MKTXPProcessor +from mktxp.basep import MKTXPProcessor, MKTXPCLIProcessor class MKTXPDispatcher: ''' Base MKTXP Commands Dispatcher @@ -31,10 +30,7 @@ class MKTXPDispatcher: def dispatch(self): args = self.option_parser.parse_options() - if args['sub_cmd'] == MKTXPCommands.VERSION: - self.print_version() - - elif args['sub_cmd'] == MKTXPCommands.INFO: + if args['sub_cmd'] == MKTXPCommands.INFO: self.print_info() elif args['sub_cmd'] == MKTXPCommands.SHOW: @@ -49,9 +45,12 @@ class MKTXPDispatcher: elif args['sub_cmd'] == MKTXPCommands.DELETE: self.delete_entry(args) - elif args['sub_cmd'] == MKTXPCommands.START: + elif args['sub_cmd'] == MKTXPCommands.EXPORT: self.start_export(args) + elif args['sub_cmd'] == MKTXPCommands.PRINT: + self.print(args) + else: # nothing to dispatch return False @@ -59,18 +58,11 @@ class MKTXPDispatcher: return True # Dispatched methods - def print_version(self): - ''' Prints MKTXP version info - ''' - version = pkg_resources.require("mktxp")[0].version - print(f'Mikrotik RouterOS Prometheus Exporter version {version}') - def print_info(self): ''' Prints MKTXP general info ''' print(f'{self.option_parser.script_name}: {self.option_parser.description}') - def show_entries(self, args): if args['config']: print(f'MKTXP data config: {config_handler.usr_conf_data_path}') @@ -100,7 +92,10 @@ class MKTXPDispatcher: editor = args['editor'] if not editor: print(f'No editor to edit the following file with: {config_handler.usr_conf_data_path}') - subprocess.check_call([editor, config_handler.usr_conf_data_path]) + if args['internal']: + subprocess.check_call([editor, config_handler.mktxp_conf_path]) + else: + subprocess.check_call([editor, config_handler.usr_conf_data_path]) def delete_entry(self, args): config_handler.unregister_entry(entry_name = args['entry_name']) @@ -108,6 +103,16 @@ class MKTXPDispatcher: def start_export(self, args): MKTXPProcessor.start() + def print(self, args): + if not (args['wifi_clients'] or args['capsman_clients']): + print("Select metric option(s) to print out, or run 'mktxp print' -h to find out more") + + if args['wifi_clients']: + MKTXPCLIProcessor.wifi_clients(args['entry_name']) + + if args['capsman_clients']: + MKTXPCLIProcessor.capsman_clients(args['entry_name']) + def main(): MKTXPDispatcher().dispatch() diff --git a/mktxp/cli/options.py b/mktxp/cli/options.py index c4cd092..64f1893 100755 --- a/mktxp/cli/options.py +++ b/mktxp/cli/options.py @@ -12,6 +12,7 @@ ## GNU General Public License for more details. import os +import pkg_resources from argparse import ArgumentParser, HelpFormatter from mktxp.cli.config.config import config_handler, MKTXPConfigKeys from mktxp.utils.utils import FSHelper, UniquePartialMatchList, run_cmd @@ -19,37 +20,38 @@ from mktxp.utils.utils import FSHelper, UniquePartialMatchList, run_cmd class MKTXPCommands: INFO = 'info' - VERSION = 'version' + EDIT = 'edit' + EXPORT = 'export' + PRINT = 'print' SHOW = 'show' ADD = 'add' - EDIT = 'edit' DELETE = 'delete' - START = 'start' @classmethod def commands_meta(cls): return ''.join(('{', f'{cls.INFO}, ', - f'{cls.VERSION}, ', + f'{cls.EDIT}, ', + f'{cls.EXPORT}, ', + f'{cls.PRINT}, ', f'{cls.SHOW}, ', f'{cls.ADD}, ', - f'{cls.EDIT}, ', - f'{cls.DELETE}, ', - f'{cls.START}', + f'{cls.DELETE}', '}')) class MKTXPOptionsParser: ''' Base MKTXP Options Parser ''' def __init__(self): - self._script_name = 'MKTXP' - self._description = \ - ''' - Prometheus Exporter for Mikrotik RouterOS. - Supports gathering metrics across multiple RouterOS devices, all easily configurable via built-in CLI interface. - Comes along with a dedicated Grafana dashboard(https://grafana.com/grafana/dashboards/13679) - - ''' + self._script_name = f'MKTXP' + version = pkg_resources.require("mktxp")[0].version + self._description = \ +f''' +Prometheus Exporter for Mikrotik RouterOS, version {version} +Supports gathering metrics across multiple RouterOS devices, all easily configurable via built-in CLI interface. +Comes along with a dedicated Grafana dashboard (https://grafana.com/grafana/dashboards/13679) +Selected metrics info can be printed on the command line. For more information, run: 'mktxp -h' +''' @property def description(self): @@ -64,7 +66,7 @@ class MKTXPOptionsParser: ''' General Options parsing workflow ''' parser = ArgumentParser(prog = self._script_name, - description = self._description, + description = 'Prometheus Exporter for Mikrotik RouterOS', formatter_class=MKTXPHelpFormatter) self.parse_global_options(parser) @@ -91,11 +93,6 @@ class MKTXPOptionsParser: subparsers.add_parser(MKTXPCommands.INFO, description = 'Displays MKTXP info', formatter_class=MKTXPHelpFormatter) - # Version command - subparsers.add_parser(MKTXPCommands.VERSION, - description = 'Displays MKTXP version info', - formatter_class=MKTXPHelpFormatter) - # Show command show_parser = subparsers.add_parser(MKTXPCommands.SHOW, description = 'Displays MKTXP config router entries', @@ -170,10 +167,14 @@ class MKTXPOptionsParser: edit_parser = subparsers.add_parser(MKTXPCommands.EDIT, description = 'Edits an existing MKTXP router entry', formatter_class=MKTXPHelpFormatter) - edit_parser.add_argument('-ed', '--editor', dest='editor', - help = f"command line editor to use ({self._system_editor()} by default)", + optional_args_group = edit_parser.add_argument_group('Optional Arguments') + optional_args_group.add_argument('-ed', '--editor', dest='editor', + help = f"Command line editor to use ({self._system_editor()} by default)", default = self._system_editor(), type = str) + optional_args_group.add_argument('-i', '--internal', dest='internal', + help = f"Edit MKTXP internal configuration (advanced)", + action = 'store_true') # Delete command delete_parser = subparsers.add_parser(MKTXPCommands.DELETE, @@ -183,10 +184,27 @@ class MKTXPOptionsParser: self._add_entry_name(required_args_group, registered_only = True, help = "Name of entry to delete") # Start command - start_parser = subparsers.add_parser(MKTXPCommands.START, - description = 'Starts exporting Miktorik Router Metrics', + start_parser = subparsers.add_parser(MKTXPCommands.EXPORT, + description = 'Starts exporting Miktorik Router Metrics to Prometheus', formatter_class=MKTXPHelpFormatter) + # Print command + print_parser = subparsers.add_parser(MKTXPCommands.PRINT, + description = 'Displays seleted metrics on the command line', + formatter_class=MKTXPHelpFormatter) + required_args_group = print_parser.add_argument_group('Required Arguments') + self._add_entry_name(required_args_group, registered_only = True, help = "Name of config RouterOS entry") + + optional_args_group = print_parser.add_argument_group('Optional Arguments') + optional_args_group.add_argument('-cc', '--capsman_clients', dest='capsman_clients', + help = "CAPsMAN clients metrics", + action = 'store_true') + + optional_args_group.add_argument('-wc', '--wifi_clients', dest='wifi_clients', + help = "WiFi clients metrics", + action = 'store_true') + + # Options checking def _check_args(self, args, parser): ''' Validation of supplied CLI arguments @@ -194,16 +212,21 @@ class MKTXPOptionsParser: # check if there is a cmd to execute self._check_cmd_args(args, parser) - if args['sub_cmd'] in (MKTXPCommands.DELETE, MKTXPCommands.SHOW): + if args['sub_cmd'] in (MKTXPCommands.DELETE, MKTXPCommands.SHOW, MKTXPCommands.PRINT): # Registered Entry name could be a partial match, need to expand if args['entry_name']: args['entry_name'] = UniquePartialMatchList(config_handler.registered_entries()).find(args['entry_name']) - elif args['sub_cmd'] == MKTXPCommands.ADD: + if args['sub_cmd'] == MKTXPCommands.ADD: if args['entry_name'] in (config_handler.registered_entries()): print(f"{args['entry_name']}: entry name already exists") parser.exit() + elif args['sub_cmd'] == MKTXPCommands.PRINT: + if not config_handler.entry(args['entry_name']).enabled: + print(f"Can not print metrics for disabled RouterOS entry: {args['entry_name']}\nRun 'mktxp edit' to review and enable it in the configuration file first") + parser.exit() + def _check_cmd_args(self, args, parser): ''' Validation of supplied CLI commands ''' @@ -223,7 +246,7 @@ class MKTXPOptionsParser: def _default_command(self): ''' If no command was specified, print INFO by default ''' - return MKTXPCommands.START + return MKTXPCommands.INFO # Internal helpers diff --git a/mktxp/cli/output/__init__.py b/mktxp/cli/output/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mktxp/cli/output/base_out.py b/mktxp/cli/output/base_out.py new file mode 100644 index 0000000..4c24b74 --- /dev/null +++ b/mktxp/cli/output/base_out.py @@ -0,0 +1,101 @@ +# coding=utf8 +## Copyright (c) 2020 Arseniy Kuznetsov +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +import re +from datetime import timedelta +from collections import namedtuple +from humanize import naturaldelta +from mktxp.cli.config.config import config_handler + + +class BaseOutputProcessor: + OutputCapsmanEntry = namedtuple('OutputCapsmanEntry', ['dhcp_name', 'dhcp_address', 'mac_address', 'rx_signal', 'interface', 'ssid', 'tx_rate', 'rx_rate', 'uptime']) + OutputWiFiEntry = namedtuple('OutputWiFiEntry', ['dhcp_name', 'dhcp_address', 'mac_address', 'signal_strength', 'signal_to_noise', 'interface', 'tx_rate', 'rx_rate', 'uptime']) + + @staticmethod + def augment_record(router_metric, registration_record, dhcp_lease_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'])) + dhcp_name = dhcp_lease_record.get('host_name') + dhcp_comment = dhcp_lease_record.get('comment') + + if dhcp_name and dhcp_comment: + dhcp_name = f'{dhcp_name[0:20]} ({dhcp_comment[0:20]})' if not router_metric.router_entry.use_comments_over_names else dhcp_comment + elif dhcp_comment: + dhcp_name = dhcp_comment + else: + dhcp_name = dhcp_lease_record.get('mac_address') if not dhcp_name else dhcp_name + dhcp_address = dhcp_lease_record.get('address', '') + except StopIteration: + dhcp_name = registration_record['mac_address'] + dhcp_address = 'No DHCP Record' + + registration_record['dhcp_name'] = dhcp_name + registration_record['dhcp_address'] = dhcp_address + + # split out tx/rx bytes + if registration_record.get('bytes'): + registration_record['tx_bytes'] = registration_record['bytes'].split(',')[0] + registration_record['rx_bytes'] = registration_record['bytes'].split(',')[1] + del registration_record['bytes'] + + registration_record['tx_rate'] = BaseOutputProcessor.parse_rates(registration_record['tx_rate']) + registration_record['rx_rate'] = BaseOutputProcessor.parse_rates(registration_record['rx_rate']) + registration_record['uptime'] = naturaldelta(BaseOutputProcessor.parse_timedelta_seconds(registration_record['uptime']), months=True, minimum_unit='seconds', when=None) + + if registration_record.get('signal_strength'): + registration_record['signal_strength'] = BaseOutputProcessor.parse_signal_strength(registration_record['signal_strength']) + if registration_record.get('rx_signal'): + registration_record['rx_signal'] = BaseOutputProcessor.parse_signal_strength(registration_record['rx_signal']) + + @staticmethod + def parse_rates(rate): + wifi_rates_rgx = config_handler.re_compiled.get('wifi_rates_rgx') + if not wifi_rates_rgx: + wifi_rates_rgx = re.compile(r'(\d*(?:\.\d*)?)([GgMmKk]bps?)') + config_handler.re_compiled['wifi_rates_rgx'] = wifi_rates_rgx + rc = wifi_rates_rgx.search(rate) + return f'{int(float(rc[1]))} {rc[2]}' + + @staticmethod + def parse_timedelta(time): + duration_interval_rgx = config_handler.re_compiled.get('duration_interval_rgx') + if not duration_interval_rgx: + duration_interval_rgx = re.compile(r'((?P\d+)w)?((?P\d+)d)?((?P\d+)h)?((?P\d+)m)?((?P\d+)s)?') + config_handler.re_compiled['duration_interval_rgx'] = duration_interval_rgx + time_dict = duration_interval_rgx.match(time).groupdict() + return timedelta(**{key: int(value) for key, value in time_dict.items() if value}) + + @staticmethod + def parse_timedelta_seconds(time): + return BaseOutputProcessor.parse_timedelta(time).total_seconds() + + @staticmethod + def parse_signal_strength(signal_strength): + wifi_signal_strength_rgx = config_handler.re_compiled.get('wifi_signal_strength_rgx') + if not wifi_signal_strength_rgx: + # wifi_signal_strength_rgx = re.compile(r'(-?\d+(?:\.\d+)?)(dBm)?') + wifi_signal_strength_rgx = re.compile(r'(-?\d+(?:\.\d+)?)') + config_handler.re_compiled['wifi_signal_strength_rgx'] = wifi_signal_strength_rgx + return wifi_signal_strength_rgx.search(signal_strength).group() + + @staticmethod + def parse_interface_rate(interface_rate): + interface_rate_rgx = config_handler.re_compiled.get('interface_rate_rgx') + if not interface_rate_rgx: + interface_rate_rgx = re.compile(r'[^.\-\d]') + config_handler.re_compiled['interface_rate_rgx'] = interface_rate_rgx + rate = lambda interface_rate: 1000 if interface_rate.find('Mbps') < 0 else 1 + return(int(float(interface_rate_rgx.sub('', interface_rate)) * rate(interface_rate))) + + diff --git a/mktxp/cli/output/capsman_out.py b/mktxp/cli/output/capsman_out.py new file mode 100644 index 0000000..33da805 --- /dev/null +++ b/mktxp/cli/output/capsman_out.py @@ -0,0 +1,52 @@ +# coding=utf8 +## Copyright (c) 2020 Arseniy Kuznetsov +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +from tabulate import tabulate +from mktxp.cli.output.base_out import BaseOutputProcessor + +class CapsmanOutput: + ''' CAPsMAN CLI Output + ''' + @staticmethod + def clients_summary(router_metric): + registration_labels = ['interface', 'ssid', 'mac_address', 'rx_signal', 'uptime', 'tx_rate', 'rx_rate'] + registration_records = router_metric.capsman_registration_table_records(registration_labels, False) + if not registration_records: + print('No CAPsMAN registration records') + return + + # translate / trim / augment registration records + dhcp_lease_labels = ['host_name', 'comment', 'address', 'mac_address'] + dhcp_lease_records = router_metric.dhcp_lease_records(dhcp_lease_labels, False) + + dhcp_rt_by_interface = {} + for registration_record in sorted(registration_records, key = lambda rt_record: rt_record['rx_signal'], reverse=True): + BaseOutputProcessor.augment_record(router_metric, registration_record, dhcp_lease_records) + + interface = registration_record['interface'] + if interface in dhcp_rt_by_interface.keys(): + dhcp_rt_by_interface[interface].append(registration_record) + else: + dhcp_rt_by_interface[interface] = [registration_record] + + num_records = 0 + output_table = [] + for key in dhcp_rt_by_interface.keys(): + for record in dhcp_rt_by_interface[key]: + output_table.append(BaseOutputProcessor.OutputCapsmanEntry(**record)) + num_records += 1 + output_table.append({}) + print() + print(tabulate(output_table, headers = "keys", tablefmt="github")) + print(tabulate([{0:'Connected Wifi Devices:', 1:num_records}], tablefmt="text")) + diff --git a/mktxp/cli/output/wifi_out.py b/mktxp/cli/output/wifi_out.py new file mode 100644 index 0000000..f7faf2f --- /dev/null +++ b/mktxp/cli/output/wifi_out.py @@ -0,0 +1,51 @@ +# coding=utf8 +## Copyright (c) 2020 Arseniy Kuznetsov +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +from tabulate import tabulate +from mktxp.cli.output.base_out import BaseOutputProcessor + +class WirelessOutput: + ''' Wireless Clients CLI Output + ''' + @staticmethod + def clients_summary(router_metric): + registration_labels = ['interface', 'mac_address', 'signal_strength', 'uptime', 'tx_rate', 'rx_rate', 'signal_to_noise'] + registration_records = router_metric.wireless_registration_table_records(registration_labels, False) + if not registration_records: + print('No wireless registration records') + return + + # translate / trim / augment registration records + dhcp_lease_labels = ['host_name', 'comment', 'address', 'mac_address'] + dhcp_lease_records = router_metric.dhcp_lease_records(dhcp_lease_labels, False) + + dhcp_rt_by_interface = {} + for registration_record in sorted(registration_records, key = lambda rt_record: rt_record['signal_strength'], reverse=True): + BaseOutputProcessor.augment_record(router_metric, registration_record, dhcp_lease_records) + + interface = registration_record['interface'] + if interface in dhcp_rt_by_interface.keys(): + dhcp_rt_by_interface[interface].append(registration_record) + else: + dhcp_rt_by_interface[interface] = [registration_record] + + num_records = 0 + output_table = [] + for key in dhcp_rt_by_interface.keys(): + for record in dhcp_rt_by_interface[key]: + output_table.append(BaseOutputProcessor.OutputWiFiEntry(**record)) + num_records += 1 + output_table.append({}) + print() + print(tabulate(output_table, headers = "keys", tablefmt="github")) + print(tabulate([{0:'Connected Wifi Devices:', 1:num_records}], tablefmt="text")) diff --git a/mktxp/collectors/base_collector.py b/mktxp/collectors/base_collector.py index 15b1c6c..4d5c31b 100644 --- a/mktxp/collectors/base_collector.py +++ b/mktxp/collectors/base_collector.py @@ -14,7 +14,6 @@ from prometheus_client.core import GaugeMetricFamily, CounterMetricFamily, InfoMetricFamily from mktxp.cli.config.config import MKTXPConfigKeys - class BaseCollector: ''' Base Collector methods diff --git a/mktxp/collectors/capsman_collector.py b/mktxp/collectors/capsman_collector.py index 6c2b80f..64dac1f 100644 --- a/mktxp/collectors/capsman_collector.py +++ b/mktxp/collectors/capsman_collector.py @@ -11,11 +11,9 @@ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. -from mktxp.utils.utils import parse_uptime +from mktxp.cli.output.base_out import BaseOutputProcessor from mktxp.cli.config.config import MKTXPConfigKeys from mktxp.collectors.base_collector import BaseCollector -from mktxp.router_metric import RouterMetric - class CapsmanCollector(BaseCollector): ''' CAPsMAN Metrics collector @@ -50,31 +48,22 @@ class CapsmanCollector(BaseCollector): # the client info metrics if router_metric.router_entry.capsman_clients: # translate / trim / augment registration records - dhcp_lease_labels = ['mac_address', 'host_name', 'comment'] + dhcp_lease_labels = ['mac_address', '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'] = f"{registration_record['mac_address']}: No DHCP registration" + BaseOutputProcessor.augment_record(router_metric, registration_record, dhcp_lease_records) - # 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_clients_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', ['dhcp_name']) yield tx_byte_metrics - rx_byte_metrics = BaseCollector.counter_collector('capsman_clients_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', ['dhcp_name']) yield rx_byte_metrics - signal_strength_metrics = BaseCollector.gauge_collector('capsman_clients_signal_strength', 'Client 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', ['dhcp_name']) yield signal_strength_metrics 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']) + registration_records, ['dhcp_name', 'dhcp_address', '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 4db6949..90427bc 100644 --- a/mktxp/collectors/dhcp_collector.py +++ b/mktxp/collectors/dhcp_collector.py @@ -13,8 +13,6 @@ from mktxp.cli.config.config import MKTXPConfigKeys from mktxp.collectors.base_collector import BaseCollector -from mktxp.router_metric import RouterMetric - class DHCPCollector(BaseCollector): ''' DHCP Metrics collector diff --git a/mktxp/collectors/health_collector.py b/mktxp/collectors/health_collector.py index f596bd4..e8ea4fd 100644 --- a/mktxp/collectors/health_collector.py +++ b/mktxp/collectors/health_collector.py @@ -12,8 +12,6 @@ ## GNU General Public License for more details. from mktxp.collectors.base_collector import BaseCollector -from mktxp.router_metric import RouterMetric - class HealthCollector(BaseCollector): ''' System Health Metrics collector diff --git a/mktxp/collectors/identity_collector.py b/mktxp/collectors/identity_collector.py index 6814aa5..da787ba 100644 --- a/mktxp/collectors/identity_collector.py +++ b/mktxp/collectors/identity_collector.py @@ -12,8 +12,6 @@ ## GNU General Public License for more details. from mktxp.collectors.base_collector import BaseCollector -from mktxp.router_metric import RouterMetric - class IdentityCollector(BaseCollector): ''' System Identity Metrics collector diff --git a/mktxp/collectors/interface_collector.py b/mktxp/collectors/interface_collector.py index b752071..eae681e 100644 --- a/mktxp/collectors/interface_collector.py +++ b/mktxp/collectors/interface_collector.py @@ -12,8 +12,6 @@ ## GNU General Public License for more details. from mktxp.collectors.base_collector import BaseCollector -from mktxp.router_metric import RouterMetric - class InterfaceCollector(BaseCollector): ''' Router Interface Metrics collector @@ -27,7 +25,8 @@ class InterfaceCollector(BaseCollector): 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']})" + interface_traffic_record['name'] = interface_traffic_record['comment'] if router_metric.router_entry.use_comments_over_names \ + else 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/mktxp_collector.py b/mktxp/collectors/mktxp_collector.py new file mode 100644 index 0000000..561ee9c --- /dev/null +++ b/mktxp/collectors/mktxp_collector.py @@ -0,0 +1,57 @@ +# coding=utf8 +## Copyright (c) 2020 Arseniy Kuznetsov +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + + +import speedtest +from datetime import datetime +from multiprocessing import Pool +from prometheus_client import Gauge +from mktxp.cli.config.config import config_handler + +result_list = [{'download': 0, 'upload': 0, 'ping': 0}] +def get_result(bandwidth_dict): + result_list.append(bandwidth_dict) + +class MKTXPCollector: + ''' MKTXP collector + ''' + def __init__(self): + self.pool = Pool() + self.last_call_timestamp = 0 + self.gauge_bandwidth = Gauge('mktxp_internet_bandwidth', 'Internet bandwidth in bits per second', ['direction']) + self.gauge_latency = Gauge('mktxp_internet_latency', 'Internet bandwidth latency in milliseconds') + + def collect(self): + if result_list: + bandwidth_dict = result_list.pop(0) + self.gauge_bandwidth.labels('download').set(bandwidth_dict["download"]) + self.gauge_bandwidth.labels('upload').set(bandwidth_dict["upload"]) + self.gauge_latency.set(bandwidth_dict["ping"]) + + ts = datetime.now().timestamp() + if (ts - self.last_call_timestamp) > config_handler._entry().bandwidth_test_interval: + self.pool.apply_async(MKTXPCollector.bandwidth_worker, callback=get_result) + self.last_call_timestamp = ts + + def __del__(self): + self.pool.close() + self.pool.join() + + @staticmethod + def bandwidth_worker(): + bandwidth_test = speedtest.Speedtest() + bandwidth_test.get_best_server() + bandwidth_test.download() + bandwidth_test.upload() + return bandwidth_test.results.dict() + diff --git a/mktxp/collectors/monitor_collector.py b/mktxp/collectors/monitor_collector.py index b37a406..06af071 100644 --- a/mktxp/collectors/monitor_collector.py +++ b/mktxp/collectors/monitor_collector.py @@ -11,10 +11,8 @@ ## 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 - +from mktxp.cli.output.base_out import BaseOutputProcessor class MonitorCollector(BaseCollector): ''' Ethernet Interface Monitor Metrics collector @@ -71,7 +69,9 @@ class MonitorCollector(BaseCollector): if rate_value: return rate_value - # ...or just calculate if it's not - rate = lambda rate_option: 1000 if rate_option.find('Mbps') < 0 else 1 - return(int(float(re.sub('[^.\-\d]', '', rate_option)) * rate(rate_option))) + # ...or just calculate in case it's not + return BaseOutputProcessor.parse_interface_rate(rate_option) + + + diff --git a/mktxp/collectors/pool_collector.py b/mktxp/collectors/pool_collector.py index dfa12b9..9ac8f3c 100644 --- a/mktxp/collectors/pool_collector.py +++ b/mktxp/collectors/pool_collector.py @@ -13,8 +13,6 @@ from mktxp.cli.config.config import MKTXPConfigKeys from mktxp.collectors.base_collector import BaseCollector -from mktxp.router_metric import RouterMetric - class PoolCollector(BaseCollector): ''' IP Pool Metrics collector diff --git a/mktxp/collectors/resource_collector.py b/mktxp/collectors/resource_collector.py index 5b18ecf..d846c2e 100644 --- a/mktxp/collectors/resource_collector.py +++ b/mktxp/collectors/resource_collector.py @@ -11,11 +11,8 @@ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. -import re -from mktxp.utils.utils import parse_uptime from mktxp.collectors.base_collector import BaseCollector -from mktxp.router_metric import RouterMetric - +from mktxp.cli.output.base_out import BaseOutputProcessor class SystemResourceCollector(BaseCollector): ''' System Resource Metrics collector @@ -67,6 +64,6 @@ class SystemResourceCollector(BaseCollector): @staticmethod def _translated_values(translated_field, value): return { - 'uptime': lambda value: parse_uptime(value) + 'uptime': lambda value: BaseOutputProcessor.parse_timedelta_seconds(value) }[translated_field](value) diff --git a/mktxp/collectors/route_collector.py b/mktxp/collectors/route_collector.py index 53ff40d..aa0d238 100644 --- a/mktxp/collectors/route_collector.py +++ b/mktxp/collectors/route_collector.py @@ -13,8 +13,6 @@ from mktxp.cli.config.config import MKTXPConfigKeys from mktxp.collectors.base_collector import BaseCollector -from mktxp.router_metric import RouterMetric - class RouteCollector(BaseCollector): ''' IP Route Metrics collector diff --git a/mktxp/collectors/wlan_collector.py b/mktxp/collectors/wlan_collector.py index 1ba5dc4..23fb583 100644 --- a/mktxp/collectors/wlan_collector.py +++ b/mktxp/collectors/wlan_collector.py @@ -11,10 +11,8 @@ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. -import re +from mktxp.cli.output.base_out import BaseOutputProcessor from mktxp.collectors.base_collector import BaseCollector -from mktxp.router_metric import RouterMetric - class WLANCollector(BaseCollector): ''' Wireless Metrics collector @@ -51,42 +49,29 @@ class WLANCollector(BaseCollector): if not registration_records: return range(0) - dhcp_lease_labels = ['mac_address', 'host_name', 'comment'] + dhcp_lease_labels = ['mac_address', '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'] + BaseOutputProcessor.augment_record(router_metric, registration_record, dhcp_lease_records) - # 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']) + tx_byte_metrics = BaseCollector.counter_collector('wlan_clients_tx_bytes', 'Number of sent packet bytes', registration_records, 'tx_bytes', ['dhcp_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']) + rx_byte_metrics = BaseCollector.counter_collector('wlan_clients_rx_bytes', 'Number of received packet bytes', registration_records, 'rx_bytes', ['dhcp_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']) + signal_strength_metrics = BaseCollector.gauge_collector('wlan_clients_signal_strength', 'Average strength of the client signal recevied by AP', registration_records, 'signal_strength', ['dhcp_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']) + signal_to_noise_metrics = BaseCollector.gauge_collector('wlan_clients_signal_to_noise', 'Client devices signal to noise ratio', registration_records, 'signal_to_noise', ['dhcp_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']) + tx_ccq_metrics = BaseCollector.gauge_collector('wlan_clients_tx_ccq', 'Client Connection Quality (CCQ) for transmit', registration_records, 'tx_ccq', ['dhcp_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']) + registration_records, ['dhcp_name', 'dhcp_address', 'rx_signal', 'ssid', 'tx_rate', 'rx_rate', 'interface', 'mac_address', 'uptime']) yield registration_metrics diff --git a/mktxp/collectors_handler.py b/mktxp/collectors_handler.py index a0d3af5..151bc33 100644 --- a/mktxp/collectors_handler.py +++ b/mktxp/collectors_handler.py @@ -21,15 +21,20 @@ from mktxp.collectors.resource_collector import SystemResourceCollector from mktxp.collectors.route_collector import RouteCollector from mktxp.collectors.wlan_collector import WLANCollector from mktxp.collectors.capsman_collector import CapsmanCollector +from mktxp.collectors.mktxp_collector import MKTXPCollector class CollectorsHandler: ''' MKTXP Collectors Handler ''' def __init__(self, metrics_handler): self.metrics_handler = metrics_handler + self.mktxpCollector = MKTXPCollector() def collect(self): - for router_metric in self.metrics_handler.router_metrics: + # process mktxp internal metrics + self.mktxpCollector.collect() + + 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() diff --git a/mktxp/metrics_handler.py b/mktxp/metrics_handler.py index 6cd2b0b..634c1c6 100644 --- a/mktxp/metrics_handler.py +++ b/mktxp/metrics_handler.py @@ -26,4 +26,15 @@ class RouterMetricsHandler: if entry.enabled: self.router_metrics.append(RouterMetric(router_name)) - + @staticmethod + def router_metric(entry_name, enabled_only = False): + router_metric = None + for router_name in config_handler.registered_entries(): + if router_name == entry_name: + if enabled_only: + entry = config_handler.entry(router_name) + if not entry.enabled: + break + router_metric = RouterMetric(router_name) + break + return router_metric diff --git a/mktxp/router_metric.py b/mktxp/router_metric.py index 91befb9..3c23515 100644 --- a/mktxp/router_metric.py +++ b/mktxp/router_metric.py @@ -59,11 +59,11 @@ class RouterMetric: print(f'Error getting system resource info from router{self.router_name}@{self.router_entry.hostname}: {exc}') return None - def dhcp_lease_records(self, dhcp_lease_labels = []): + def dhcp_lease_records(self, dhcp_lease_labels = [], add_router_id = True): try: #dhcp_lease_records = self.api_connection.router_api().get_resource('/ip/dhcp-server/lease').get(status='bound') dhcp_lease_records = self.api_connection.router_api().get_resource('/ip/dhcp-server/lease').call('print', {'active':''}) - return self._trimmed_records(dhcp_lease_records, dhcp_lease_labels) + return self._trimmed_records(dhcp_lease_records, dhcp_lease_labels, add_router_id) except Exception as exc: print(f'Error getting dhcp info from router{self.router_name}@{self.router_entry.hostname}: {exc}') return None @@ -89,7 +89,8 @@ class RouterMetric: 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]})" + interface_monitor_record['name'] = interface_name[1] if self.router_entry.use_comments_over_names else \ + f"{interface_name[0]} ({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}') @@ -119,10 +120,10 @@ class RouterMetric: 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 = []): + def wireless_registration_table_records(self, registration_table_labels = [], add_router_id = True): try: registration_table_records = self.api_connection.router_api().get_resource('/interface/wireless/registration-table').get() - return self._trimmed_records(registration_table_records, registration_table_labels) + return self._trimmed_records(registration_table_records, registration_table_labels, add_router_id) except Exception as exc: print(f'Error getting wireless registration table info from router{self.router_name}@{self.router_entry.hostname}: {exc}') return None @@ -135,16 +136,16 @@ class RouterMetric: print(f'Error getting caps-man remote caps info from router{self.router_name}@{self.router_entry.hostname}: {exc}') return None - def capsman_registration_table_records(self, registration_table_labels = []): + def capsman_registration_table_records(self, registration_table_labels = [], add_router_id = True): try: registration_table_records = self.api_connection.router_api().get_resource('/caps-man/registration-table').get() - return self._trimmed_records(registration_table_records, registration_table_labels) + return self._trimmed_records(registration_table_records, registration_table_labels, add_router_id) except Exception as exc: print(f'Error getting caps-man registration table info from router{self.router_name}@{self.router_entry.hostname}: {exc}') return None # Helpers - def _trimmed_records(self, router_records, metric_labels): + def _trimmed_records(self, router_records, metric_labels, add_router_id = True): if len(metric_labels) == 0 and len(router_records) > 0: metric_labels = router_records[0].keys() metric_labels = set(metric_labels) @@ -153,8 +154,9 @@ class RouterMetric: dash2_ = lambda x : x.replace('-', '_') for router_record in router_records: translated_record = {dash2_(key): value for (key, value) in router_record.items() if dash2_(key) in metric_labels} - for key, value in self.router_id.items(): - translated_record[key] = value + if add_router_id: + for key, value in self.router_id.items(): + translated_record[key] = value labeled_records.append(translated_record) return labeled_records diff --git a/mktxp/utils/utils.py b/mktxp/utils/utils.py index bd56284..e2128cf 100755 --- a/mktxp/utils/utils.py +++ b/mktxp/utils/utils.py @@ -12,14 +12,14 @@ ## GNU General Public License for more details. import os, sys, shlex, tempfile, shutil, re -from datetime import timedelta import subprocess, hashlib -from collections import Iterable +from collections.abc import Iterable from contextlib import contextmanager +from multiprocessing import Process, Event + ''' Utilities / Helpers ''' - @contextmanager def temp_dir(quiet = True): ''' Temp dir context manager @@ -67,11 +67,6 @@ def get_last_digit(str_to_search): else: return -1 -def parse_uptime(time): - time_dict = re.match(r'((?P\d+)w)?((?P\d+)d)?((?P\d+)h)?((?P\d+)m)?((?P\d+)s)?', time).groupdict() - return timedelta(**{key: int(value) for key, value in time_dict.items() if value}).total_seconds() - - class FSHelper: ''' File System ops helper ''' @@ -222,3 +217,41 @@ class UniquePartialMatchList(list): either "equals to element" or "contained by exactly one element" ''' return True if self.find(partialMatch) else False + + +class RepeatableTimer: + def __init__(self, interval, func, args=[], kwargs={}, process_name = None, repeatable = True, restartable = False): + self.process_name = process_name + self.interval = interval + self.restartable = restartable + + self.func = func + self.args = args + self.kwargs = kwargs + + self.finished = Event() + self.run_once = Event() + if not repeatable: + self.run_once.set() + self.process = Process(name = self.process_name, target=self._execute) + + def start(self): + if self.restartable: + self.finished.clear() + self.process = Process(name = self.process_name, target=self._execute, daemon=True) + self.process.start() + + def stop(self): + self.finished.set() + if self.process.is_alive: + self.process.join() + + def _execute(self): + while True: + self.func(*self.args, **self.kwargs) + if self.finished.is_set() or self.run_once.is_set(): + break + self.finished.wait(self.interval) + + + diff --git a/setup.py b/setup.py index 71a7802..a05921d 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.20', + version='0.21', url='https://github.com/akpw/mktxp', @@ -43,7 +43,13 @@ setup( keywords = 'Mikrotik RouterOS Prometheus Exporter', - install_requires = ['prometheus-client>=0.9.0', 'RouterOS-api>=0.17.0', 'configobj>=5.0.6'], + install_requires = ['prometheus-client>=0.9.0', + 'RouterOS-api>=0.17.0', + 'configobj>=5.0.6', + 'humanize>=3.2.0', + 'tabulate>=0.8.7', + 'speedtest-cli>=2.1.2' + ], test_suite = 'tests.mktxp_test_suite',