cli metrics, fixes

This commit is contained in:
Arseniy Kuznetsov
2021-01-17 20:34:17 +01:00
parent 8faa12786f
commit 31d0464eb2
28 changed files with 499 additions and 147 deletions

View File

@@ -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`

View File

@@ -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)

View File

@@ -17,3 +17,4 @@
initial_delay_on_failure = 120
max_delay_on_failure = 900
delay_inc_div = 5
bandwidth_test_interval = 420

View File

@@ -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()

View File

@@ -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
use_comments_over_names = False # when available, use comments instead of interface names

View File

@@ -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()

View File

@@ -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

View File

View File

@@ -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<weeks>\d+)w)?((?P<days>\d+)d)?((?P<hours>\d+)h)?((?P<minutes>\d+)m)?((?P<seconds>\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)))

View File

@@ -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"))

View File

@@ -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"))

View File

@@ -14,7 +14,6 @@
from prometheus_client.core import GaugeMetricFamily, CounterMetricFamily, InfoMetricFamily
from mktxp.cli.config.config import MKTXPConfigKeys
class BaseCollector:
''' Base Collector methods

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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<weeks>\d+)w)?((?P<days>\d+)d)?((?P<hours>\d+)h)?((?P<minutes>\d+)m)?((?P<seconds>\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)

View File

@@ -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',