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 ## Full description of CLI Commands
### mktxp ### mktxp
. action commands: . 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 .. edit Open MKTXP configuration file in your editor of choice
.. add Adds MKTXP RouterOS configuration entry from the command line .. add Adds MKTXP RouterOS configuration entry from the command line
.. show Shows MKTXP configuration entries on the command line .. show Shows MKTXP configuration entries on the command line
.. delete Deletes a MKTXP RouterOS configuration entry from 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 ## Installing Development version
- Clone the repo, then run: `$ python setup.py develop` - Clone the repo, then run: `$ python setup.py develop`
**Running Tests** **Running Tests**
- TDB - TDB
- Run via: `$ python setup.py test` - Run via: `$ python setup.py test`

View File

@@ -11,7 +11,6 @@
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details. ## GNU General Public License for more details.
from http.server import HTTPServer from http.server import HTTPServer
from datetime import datetime from datetime import datetime
from prometheus_client.core import REGISTRY 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.collectors_handler import CollectorsHandler
from mktxp.metrics_handler import RouterMetricsHandler from mktxp.metrics_handler import RouterMetricsHandler
from mktxp.cli.output.capsman_out import CapsmanOutput
from mktxp.cli.output.wifi_out import WirelessOutput
class MKTXPProcessor: class MKTXPProcessor:
''' Base Export Processing ''' Base Export Processing
@@ -35,5 +36,22 @@ class MKTXPProcessor:
server_address = ('', port) server_address = ('', port)
httpd = server_class(server_address, handler_class) httpd = server_class(server_address, handler_class)
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 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() 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 initial_delay_on_failure = 120
max_delay_on_failure = 900 max_delay_on_failure = 900
delay_inc_div = 5 delay_inc_div = 5
bandwidth_test_interval = 420

View File

@@ -52,10 +52,14 @@ class MKTXPConfigKeys:
MKTXP_INITIAL_DELAY = 'initial_delay_on_failure' MKTXP_INITIAL_DELAY = 'initial_delay_on_failure'
MKTXP_MAX_DELAY = 'max_delay_on_failure' MKTXP_MAX_DELAY = 'max_delay_on_failure'
MKTXP_INC_DIV = 'delay_inc_div' MKTXP_INC_DIV = 'delay_inc_div'
MKTXP_BANDWIDTH_TEST_INTERVAL = 'bandwidth_test_interval'
# UnRegistered entries placeholder # UnRegistered entries placeholder
NO_ENTRIES_REGISTERED = 'NoEntriesRegistered' NO_ENTRIES_REGISTERED = 'NoEntriesRegistered'
MKTXP_USE_COMMENTS_OVER_NAMES = 'use_comments_over_names'
# Base router id labels # Base router id labels
ROUTERBOARD_NAME = 'routerboard_name' ROUTERBOARD_NAME = 'routerboard_name'
ROUTERBOARD_ADDRESS = 'routerboard_address' ROUTERBOARD_ADDRESS = 'routerboard_address'
@@ -68,13 +72,15 @@ class MKTXPConfigKeys:
DEFAULT_MKTXP_INITIAL_DELAY = 120 DEFAULT_MKTXP_INITIAL_DELAY = 120
DEFAULT_MKTXP_MAX_DELAY = 900 DEFAULT_MKTXP_MAX_DELAY = 900
DEFAULT_MKTXP_INC_DIV = 5 DEFAULT_MKTXP_INC_DIV = 5
DEFAULT_MKTXP_BANDWIDTH_TEST_INTERVAL = 420
BOOLEAN_KEYS = (ENABLED_KEY, SSL_KEY, NO_SSL_CERTIFICATE, SSL_CERTIFICATE_VERIFY, 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_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] STR_KEYS = (HOST_KEY, USER_KEY, PASSWD_KEY)
INT_KEYS = [PORT_KEY, MKTXP_SOCKET_TIMEOUT, MKTXP_INITIAL_DELAY, MKTXP_MAX_DELAY, MKTXP_INC_DIV] MKTXP_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 nane
MKTXP_CONFIG_ENTRY_NAME = 'MKTXP' 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_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_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, _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): 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.usr_conf_data_path, 'mktxp/cli/config/mktxp.conf')
self._create_os_path(self.mktxp_conf_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() self._read_from_disk()
def _read_from_disk(self): def _read_from_disk(self):
@@ -219,11 +228,13 @@ class MKTXPConfigHandler:
# Helpers # Helpers
def entry_reader(self, entry_name): def entry_reader(self, entry_name):
entry_reader = {} entry_reader = {}
write_needed = False
for key in MKTXPConfigKeys.BOOLEAN_KEYS: for key in MKTXPConfigKeys.BOOLEAN_KEYS:
if self.config[entry_name].get(key): if self.config[entry_name].get(key):
entry_reader[key] = self.config[entry_name].as_bool(key) entry_reader[key] = self.config[entry_name].as_bool(key)
else: else:
entry_reader[key] = False entry_reader[key] = False
write_needed = True # read from disk next time
for key in MKTXPConfigKeys.STR_KEYS: for key in MKTXPConfigKeys.STR_KEYS:
entry_reader[key] = self.config[entry_name][key] 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) entry_reader[MKTXPConfigKeys.PORT_KEY] = self.config[entry_name].as_int(MKTXPConfigKeys.PORT_KEY)
else: else:
entry_reader[MKTXPConfigKeys.PORT_KEY] = self._default_value_for_key(MKTXPConfigKeys.SSL_KEY, entry_reader[MKTXPConfigKeys.SSL_KEY]) 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 return entry_reader
def _entry_reader(self): def _entry_reader(self):
_entry_reader = {} _entry_reader = {}
entry_name = MKTXPConfigKeys.MKTXP_CONFIG_ENTRY_NAME 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): if self._config[entry_name].get(key):
_entry_reader[key] = self._config[entry_name].as_int(key) _entry_reader[key] = self._config[entry_name].as_int(key)
else: else:
_entry_reader[key] = self._default_value_for_key(key) _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 return _entry_reader
def _default_value_for_key(self, key, value = None): 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_SOCKET_TIMEOUT: lambda value: MKTXPConfigKeys.DEFAULT_MKTXP_SOCKET_TIMEOUT,
MKTXPConfigKeys.MKTXP_INITIAL_DELAY: lambda value: MKTXPConfigKeys.DEFAULT_MKTXP_INITIAL_DELAY, MKTXPConfigKeys.MKTXP_INITIAL_DELAY: lambda value: MKTXPConfigKeys.DEFAULT_MKTXP_INITIAL_DELAY,
MKTXPConfigKeys.MKTXP_MAX_DELAY: lambda value: MKTXPConfigKeys.DEFAULT_MKTXP_MAX_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) }[key](value)
# Simplest possible Singleton impl # Simplest possible Singleton impl
config_handler = MKTXPConfigHandler() config_handler = MKTXPConfigHandler()

View File

@@ -35,4 +35,4 @@
capsman = True capsman = True
capsman_clients = 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 sys
import subprocess import subprocess
import pkg_resources
import mktxp.cli.checks.chk_pv import mktxp.cli.checks.chk_pv
from mktxp.utils.utils import run_cmd from mktxp.utils.utils import run_cmd
from mktxp.cli.options import MKTXPOptionsParser, MKTXPCommands from mktxp.cli.options import MKTXPOptionsParser, MKTXPCommands
from mktxp.cli.config.config import config_handler, ConfigEntry from mktxp.cli.config.config import config_handler, ConfigEntry
from mktxp.basep import MKTXPProcessor from mktxp.basep import MKTXPProcessor, MKTXPCLIProcessor
class MKTXPDispatcher: class MKTXPDispatcher:
''' Base MKTXP Commands Dispatcher ''' Base MKTXP Commands Dispatcher
@@ -31,10 +30,7 @@ class MKTXPDispatcher:
def dispatch(self): def dispatch(self):
args = self.option_parser.parse_options() args = self.option_parser.parse_options()
if args['sub_cmd'] == MKTXPCommands.VERSION: if args['sub_cmd'] == MKTXPCommands.INFO:
self.print_version()
elif args['sub_cmd'] == MKTXPCommands.INFO:
self.print_info() self.print_info()
elif args['sub_cmd'] == MKTXPCommands.SHOW: elif args['sub_cmd'] == MKTXPCommands.SHOW:
@@ -49,9 +45,12 @@ class MKTXPDispatcher:
elif args['sub_cmd'] == MKTXPCommands.DELETE: elif args['sub_cmd'] == MKTXPCommands.DELETE:
self.delete_entry(args) self.delete_entry(args)
elif args['sub_cmd'] == MKTXPCommands.START: elif args['sub_cmd'] == MKTXPCommands.EXPORT:
self.start_export(args) self.start_export(args)
elif args['sub_cmd'] == MKTXPCommands.PRINT:
self.print(args)
else: else:
# nothing to dispatch # nothing to dispatch
return False return False
@@ -59,18 +58,11 @@ class MKTXPDispatcher:
return True return True
# Dispatched methods # 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): def print_info(self):
''' Prints MKTXP general info ''' Prints MKTXP general info
''' '''
print(f'{self.option_parser.script_name}: {self.option_parser.description}') print(f'{self.option_parser.script_name}: {self.option_parser.description}')
def show_entries(self, args): def show_entries(self, args):
if args['config']: if args['config']:
print(f'MKTXP data config: {config_handler.usr_conf_data_path}') print(f'MKTXP data config: {config_handler.usr_conf_data_path}')
@@ -100,7 +92,10 @@ class MKTXPDispatcher:
editor = args['editor'] editor = args['editor']
if not editor: if not editor:
print(f'No editor to edit the following file with: {config_handler.usr_conf_data_path}') 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): def delete_entry(self, args):
config_handler.unregister_entry(entry_name = args['entry_name']) config_handler.unregister_entry(entry_name = args['entry_name'])
@@ -108,6 +103,16 @@ class MKTXPDispatcher:
def start_export(self, args): def start_export(self, args):
MKTXPProcessor.start() 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(): def main():
MKTXPDispatcher().dispatch() MKTXPDispatcher().dispatch()

View File

@@ -12,6 +12,7 @@
## GNU General Public License for more details. ## GNU General Public License for more details.
import os import os
import pkg_resources
from argparse import ArgumentParser, HelpFormatter from argparse import ArgumentParser, HelpFormatter
from mktxp.cli.config.config import config_handler, MKTXPConfigKeys from mktxp.cli.config.config import config_handler, MKTXPConfigKeys
from mktxp.utils.utils import FSHelper, UniquePartialMatchList, run_cmd from mktxp.utils.utils import FSHelper, UniquePartialMatchList, run_cmd
@@ -19,37 +20,38 @@ from mktxp.utils.utils import FSHelper, UniquePartialMatchList, run_cmd
class MKTXPCommands: class MKTXPCommands:
INFO = 'info' INFO = 'info'
VERSION = 'version' EDIT = 'edit'
EXPORT = 'export'
PRINT = 'print'
SHOW = 'show' SHOW = 'show'
ADD = 'add' ADD = 'add'
EDIT = 'edit'
DELETE = 'delete' DELETE = 'delete'
START = 'start'
@classmethod @classmethod
def commands_meta(cls): def commands_meta(cls):
return ''.join(('{', return ''.join(('{',
f'{cls.INFO}, ', f'{cls.INFO}, ',
f'{cls.VERSION}, ', f'{cls.EDIT}, ',
f'{cls.EXPORT}, ',
f'{cls.PRINT}, ',
f'{cls.SHOW}, ', f'{cls.SHOW}, ',
f'{cls.ADD}, ', f'{cls.ADD}, ',
f'{cls.EDIT}, ', f'{cls.DELETE}',
f'{cls.DELETE}, ',
f'{cls.START}',
'}')) '}'))
class MKTXPOptionsParser: class MKTXPOptionsParser:
''' Base MKTXP Options Parser ''' Base MKTXP Options Parser
''' '''
def __init__(self): def __init__(self):
self._script_name = 'MKTXP' self._script_name = f'MKTXP'
self._description = \ version = pkg_resources.require("mktxp")[0].version
''' self._description = \
Prometheus Exporter for Mikrotik RouterOS. f'''
Supports gathering metrics across multiple RouterOS devices, all easily configurable via built-in CLI interface. Prometheus Exporter for Mikrotik RouterOS, version {version}
Comes along with a dedicated Grafana dashboard(https://grafana.com/grafana/dashboards/13679) 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 @property
def description(self): def description(self):
@@ -64,7 +66,7 @@ class MKTXPOptionsParser:
''' General Options parsing workflow ''' General Options parsing workflow
''' '''
parser = ArgumentParser(prog = self._script_name, parser = ArgumentParser(prog = self._script_name,
description = self._description, description = 'Prometheus Exporter for Mikrotik RouterOS',
formatter_class=MKTXPHelpFormatter) formatter_class=MKTXPHelpFormatter)
self.parse_global_options(parser) self.parse_global_options(parser)
@@ -91,11 +93,6 @@ class MKTXPOptionsParser:
subparsers.add_parser(MKTXPCommands.INFO, subparsers.add_parser(MKTXPCommands.INFO,
description = 'Displays MKTXP info', description = 'Displays MKTXP info',
formatter_class=MKTXPHelpFormatter) formatter_class=MKTXPHelpFormatter)
# Version command
subparsers.add_parser(MKTXPCommands.VERSION,
description = 'Displays MKTXP version info',
formatter_class=MKTXPHelpFormatter)
# Show command # Show command
show_parser = subparsers.add_parser(MKTXPCommands.SHOW, show_parser = subparsers.add_parser(MKTXPCommands.SHOW,
description = 'Displays MKTXP config router entries', description = 'Displays MKTXP config router entries',
@@ -170,10 +167,14 @@ class MKTXPOptionsParser:
edit_parser = subparsers.add_parser(MKTXPCommands.EDIT, edit_parser = subparsers.add_parser(MKTXPCommands.EDIT,
description = 'Edits an existing MKTXP router entry', description = 'Edits an existing MKTXP router entry',
formatter_class=MKTXPHelpFormatter) formatter_class=MKTXPHelpFormatter)
edit_parser.add_argument('-ed', '--editor', dest='editor', optional_args_group = edit_parser.add_argument_group('Optional Arguments')
help = f"command line editor to use ({self._system_editor()} by default)", 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(), default = self._system_editor(),
type = str) type = str)
optional_args_group.add_argument('-i', '--internal', dest='internal',
help = f"Edit MKTXP internal configuration (advanced)",
action = 'store_true')
# Delete command # Delete command
delete_parser = subparsers.add_parser(MKTXPCommands.DELETE, 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") self._add_entry_name(required_args_group, registered_only = True, help = "Name of entry to delete")
# Start command # Start command
start_parser = subparsers.add_parser(MKTXPCommands.START, start_parser = subparsers.add_parser(MKTXPCommands.EXPORT,
description = 'Starts exporting Miktorik Router Metrics', description = 'Starts exporting Miktorik Router Metrics to Prometheus',
formatter_class=MKTXPHelpFormatter) 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 # Options checking
def _check_args(self, args, parser): def _check_args(self, args, parser):
''' Validation of supplied CLI arguments ''' Validation of supplied CLI arguments
@@ -194,16 +212,21 @@ class MKTXPOptionsParser:
# check if there is a cmd to execute # check if there is a cmd to execute
self._check_cmd_args(args, parser) 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 # Registered Entry name could be a partial match, need to expand
if args['entry_name']: if args['entry_name']:
args['entry_name'] = UniquePartialMatchList(config_handler.registered_entries()).find(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()): if args['entry_name'] in (config_handler.registered_entries()):
print(f"{args['entry_name']}: entry name already exists") print(f"{args['entry_name']}: entry name already exists")
parser.exit() 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): def _check_cmd_args(self, args, parser):
''' Validation of supplied CLI commands ''' Validation of supplied CLI commands
''' '''
@@ -223,7 +246,7 @@ class MKTXPOptionsParser:
def _default_command(self): def _default_command(self):
''' If no command was specified, print INFO by default ''' If no command was specified, print INFO by default
''' '''
return MKTXPCommands.START return MKTXPCommands.INFO
# Internal helpers # 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

@@ -15,7 +15,6 @@
from prometheus_client.core import GaugeMetricFamily, CounterMetricFamily, InfoMetricFamily from prometheus_client.core import GaugeMetricFamily, CounterMetricFamily, InfoMetricFamily
from mktxp.cli.config.config import MKTXPConfigKeys from mktxp.cli.config.config import MKTXPConfigKeys
class BaseCollector: class BaseCollector:
''' Base Collector methods ''' Base Collector methods
For use by custom collectors For use by custom collectors

View File

@@ -11,11 +11,9 @@
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details. ## 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.cli.config.config import MKTXPConfigKeys
from mktxp.collectors.base_collector import BaseCollector from mktxp.collectors.base_collector import BaseCollector
from mktxp.router_metric import RouterMetric
class CapsmanCollector(BaseCollector): class CapsmanCollector(BaseCollector):
''' CAPsMAN Metrics collector ''' CAPsMAN Metrics collector
@@ -50,31 +48,22 @@ class CapsmanCollector(BaseCollector):
# the client info metrics # the client info metrics
if router_metric.router_entry.capsman_clients: if router_metric.router_entry.capsman_clients:
# translate / trim / augment registration records # 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) dhcp_lease_records = router_metric.dhcp_lease_records(dhcp_lease_labels)
for registration_record in registration_records: for registration_record in registration_records:
try: BaseOutputProcessor.augment_record(router_metric, registration_record, dhcp_lease_records)
dhcp_lease_record = next((dhcp_lease_record for dhcp_lease_record in dhcp_lease_records if dhcp_lease_record['mac_address']==registration_record['mac_address']))
registration_record['name'] = dhcp_lease_record.get('comment', dhcp_lease_record.get('host_name', dhcp_lease_record.get('mac_address')))
except StopIteration:
registration_record['name'] = f"{registration_record['mac_address']}: No DHCP registration"
# split out tx/rx bytes tx_byte_metrics = BaseCollector.counter_collector('capsman_clients_tx_bytes', 'Number of sent packet bytes', registration_records, 'tx_bytes', ['dhcp_name'])
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'])
yield tx_byte_metrics 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 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 yield signal_strength_metrics
registration_metrics = BaseCollector.info_collector('capsman_clients_devices', 'Registered client devices info', registration_metrics = BaseCollector.info_collector('capsman_clients_devices', 'Registered client devices info',
registration_records, ['name', 'rx_signal', 'ssid', 'tx_rate', 'rx_rate', 'interface', 'mac_address', 'uptime']) registration_records, ['dhcp_name', 'dhcp_address', 'rx_signal', 'ssid', 'tx_rate', 'rx_rate', 'interface', 'mac_address', 'uptime'])
yield registration_metrics yield registration_metrics

View File

@@ -13,8 +13,6 @@
from mktxp.cli.config.config import MKTXPConfigKeys from mktxp.cli.config.config import MKTXPConfigKeys
from mktxp.collectors.base_collector import BaseCollector from mktxp.collectors.base_collector import BaseCollector
from mktxp.router_metric import RouterMetric
class DHCPCollector(BaseCollector): class DHCPCollector(BaseCollector):
''' DHCP Metrics collector ''' DHCP Metrics collector

View File

@@ -12,8 +12,6 @@
## GNU General Public License for more details. ## GNU General Public License for more details.
from mktxp.collectors.base_collector import BaseCollector from mktxp.collectors.base_collector import BaseCollector
from mktxp.router_metric import RouterMetric
class HealthCollector(BaseCollector): class HealthCollector(BaseCollector):
''' System Health Metrics collector ''' System Health Metrics collector

View File

@@ -12,8 +12,6 @@
## GNU General Public License for more details. ## GNU General Public License for more details.
from mktxp.collectors.base_collector import BaseCollector from mktxp.collectors.base_collector import BaseCollector
from mktxp.router_metric import RouterMetric
class IdentityCollector(BaseCollector): class IdentityCollector(BaseCollector):
''' System Identity Metrics collector ''' System Identity Metrics collector

View File

@@ -12,8 +12,6 @@
## GNU General Public License for more details. ## GNU General Public License for more details.
from mktxp.collectors.base_collector import BaseCollector from mktxp.collectors.base_collector import BaseCollector
from mktxp.router_metric import RouterMetric
class InterfaceCollector(BaseCollector): class InterfaceCollector(BaseCollector):
''' Router Interface Metrics collector ''' Router Interface Metrics collector
@@ -27,7 +25,8 @@ class InterfaceCollector(BaseCollector):
for interface_traffic_record in interface_traffic_records: for interface_traffic_record in interface_traffic_records:
if interface_traffic_record.get('comment'): 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']) rx_byte_metric = BaseCollector.counter_collector('interface_rx_byte', 'Number of received bytes', interface_traffic_records, 'rx_byte', ['name'])
yield rx_byte_metric 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 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details. ## GNU General Public License for more details.
import re
from mktxp.collectors.base_collector import BaseCollector from mktxp.collectors.base_collector import BaseCollector
from mktxp.router_metric import RouterMetric from mktxp.cli.output.base_out import BaseOutputProcessor
class MonitorCollector(BaseCollector): class MonitorCollector(BaseCollector):
''' Ethernet Interface Monitor Metrics collector ''' Ethernet Interface Monitor Metrics collector
@@ -71,7 +69,9 @@ class MonitorCollector(BaseCollector):
if rate_value: if rate_value:
return rate_value return rate_value
# ...or just calculate if it's not # ...or just calculate in case it's not
rate = lambda rate_option: 1000 if rate_option.find('Mbps') < 0 else 1 return BaseOutputProcessor.parse_interface_rate(rate_option)
return(int(float(re.sub('[^.\-\d]', '', rate_option)) * rate(rate_option)))

View File

@@ -13,8 +13,6 @@
from mktxp.cli.config.config import MKTXPConfigKeys from mktxp.cli.config.config import MKTXPConfigKeys
from mktxp.collectors.base_collector import BaseCollector from mktxp.collectors.base_collector import BaseCollector
from mktxp.router_metric import RouterMetric
class PoolCollector(BaseCollector): class PoolCollector(BaseCollector):
''' IP Pool Metrics collector ''' IP Pool Metrics collector

View File

@@ -11,11 +11,8 @@
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details. ## 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.collectors.base_collector import BaseCollector
from mktxp.router_metric import RouterMetric from mktxp.cli.output.base_out import BaseOutputProcessor
class SystemResourceCollector(BaseCollector): class SystemResourceCollector(BaseCollector):
''' System Resource Metrics collector ''' System Resource Metrics collector
@@ -67,6 +64,6 @@ class SystemResourceCollector(BaseCollector):
@staticmethod @staticmethod
def _translated_values(translated_field, value): def _translated_values(translated_field, value):
return { return {
'uptime': lambda value: parse_uptime(value) 'uptime': lambda value: BaseOutputProcessor.parse_timedelta_seconds(value)
}[translated_field](value) }[translated_field](value)

View File

@@ -13,8 +13,6 @@
from mktxp.cli.config.config import MKTXPConfigKeys from mktxp.cli.config.config import MKTXPConfigKeys
from mktxp.collectors.base_collector import BaseCollector from mktxp.collectors.base_collector import BaseCollector
from mktxp.router_metric import RouterMetric
class RouteCollector(BaseCollector): class RouteCollector(BaseCollector):
''' IP Route Metrics collector ''' IP Route Metrics collector

View File

@@ -11,10 +11,8 @@
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details. ## 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.collectors.base_collector import BaseCollector
from mktxp.router_metric import RouterMetric
class WLANCollector(BaseCollector): class WLANCollector(BaseCollector):
''' Wireless Metrics collector ''' Wireless Metrics collector
@@ -51,42 +49,29 @@ class WLANCollector(BaseCollector):
if not registration_records: if not registration_records:
return range(0) 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) dhcp_lease_records = router_metric.dhcp_lease_records(dhcp_lease_labels)
for registration_record in registration_records: for registration_record in registration_records:
try: BaseOutputProcessor.augment_record(router_metric, registration_record, dhcp_lease_records)
dhcp_lease_record = next((dhcp_lease_record for dhcp_lease_record in dhcp_lease_records if dhcp_lease_record['mac_address']==registration_record['mac_address']))
registration_record['name'] = dhcp_lease_record.get('comment', dhcp_lease_record.get('host_name', dhcp_lease_record.get('mac_address')))
except StopIteration:
registration_record['name'] = registration_record['mac_address']
# split out tx/rx bytes tx_byte_metrics = BaseCollector.counter_collector('wlan_clients_tx_bytes', 'Number of sent packet bytes', registration_records, 'tx_bytes', ['dhcp_name'])
registration_record['tx_bytes'] = registration_record['bytes'].split(',')[0]
registration_record['rx_bytes'] = registration_record['bytes'].split(',')[1]
# average signal strength
registration_record['signal_strength'] = re.search(r'-\d+', registration_record['signal_strength']).group()
del registration_record['bytes']
tx_byte_metrics = BaseCollector.counter_collector('wlan_clients_tx_bytes', 'Number of sent packet bytes', registration_records, 'tx_bytes', ['name'])
yield tx_byte_metrics 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 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 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 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 yield tx_ccq_metrics
registration_metrics = BaseCollector.info_collector('wlan_clients_devices', 'Client devices info', 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 yield registration_metrics

View File

@@ -21,14 +21,19 @@ from mktxp.collectors.resource_collector import SystemResourceCollector
from mktxp.collectors.route_collector import RouteCollector from mktxp.collectors.route_collector import RouteCollector
from mktxp.collectors.wlan_collector import WLANCollector from mktxp.collectors.wlan_collector import WLANCollector
from mktxp.collectors.capsman_collector import CapsmanCollector from mktxp.collectors.capsman_collector import CapsmanCollector
from mktxp.collectors.mktxp_collector import MKTXPCollector
class CollectorsHandler: class CollectorsHandler:
''' MKTXP Collectors Handler ''' MKTXP Collectors Handler
''' '''
def __init__(self, metrics_handler): def __init__(self, metrics_handler):
self.metrics_handler = metrics_handler self.metrics_handler = metrics_handler
self.mktxpCollector = MKTXPCollector()
def collect(self): def collect(self):
# process mktxp internal metrics
self.mktxpCollector.collect()
for router_metric in self.metrics_handler.router_metrics: for router_metric in self.metrics_handler.router_metrics:
if not router_metric.api_connection.is_connected(): if not router_metric.api_connection.is_connected():
# let's pick up on things in the next run # let's pick up on things in the next run

View File

@@ -26,4 +26,15 @@ class RouterMetricsHandler:
if entry.enabled: if entry.enabled:
self.router_metrics.append(RouterMetric(router_name)) 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}') print(f'Error getting system resource info from router{self.router_name}@{self.router_entry.hostname}: {exc}')
return None return None
def dhcp_lease_records(self, dhcp_lease_labels = []): def dhcp_lease_records(self, dhcp_lease_labels = [], add_router_id = True):
try: 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').get(status='bound')
dhcp_lease_records = self.api_connection.router_api().get_resource('/ip/dhcp-server/lease').call('print', {'active':''}) 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: except Exception as exc:
print(f'Error getting dhcp info from router{self.router_name}@{self.router_entry.hostname}: {exc}') print(f'Error getting dhcp info from router{self.router_name}@{self.router_entry.hostname}: {exc}')
return None return None
@@ -89,7 +89,8 @@ class RouterMetric:
for interface_monitor_record in interface_monitor_records: for interface_monitor_record in interface_monitor_records:
for interface_name in interface_names: for interface_name in interface_names:
if interface_name[1] and interface_name[0] == interface_monitor_record['name']: 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) return self._trimmed_records(interface_monitor_records, interface_monitor_labels)
except Exception as exc: except Exception as exc:
print(f'Error getting {kind} interface monitor info from router{self.router_name}@{self.router_entry.hostname}: {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}') print(f'Error getting routes info from router{self.router_name}@{self.router_entry.hostname}: {exc}')
return None 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: try:
registration_table_records = self.api_connection.router_api().get_resource('/interface/wireless/registration-table').get() 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: except Exception as exc:
print(f'Error getting wireless registration table info from router{self.router_name}@{self.router_entry.hostname}: {exc}') print(f'Error getting wireless registration table info from router{self.router_name}@{self.router_entry.hostname}: {exc}')
return None 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}') print(f'Error getting caps-man remote caps info from router{self.router_name}@{self.router_entry.hostname}: {exc}')
return None 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: try:
registration_table_records = self.api_connection.router_api().get_resource('/caps-man/registration-table').get() 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: except Exception as exc:
print(f'Error getting caps-man registration table info from router{self.router_name}@{self.router_entry.hostname}: {exc}') print(f'Error getting caps-man registration table info from router{self.router_name}@{self.router_entry.hostname}: {exc}')
return None return None
# Helpers # 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: if len(metric_labels) == 0 and len(router_records) > 0:
metric_labels = router_records[0].keys() metric_labels = router_records[0].keys()
metric_labels = set(metric_labels) metric_labels = set(metric_labels)
@@ -153,8 +154,9 @@ class RouterMetric:
dash2_ = lambda x : x.replace('-', '_') dash2_ = lambda x : x.replace('-', '_')
for router_record in router_records: for router_record in router_records:
translated_record = {dash2_(key): value for (key, value) in router_record.items() if dash2_(key) in metric_labels} 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(): if add_router_id:
translated_record[key] = value for key, value in self.router_id.items():
translated_record[key] = value
labeled_records.append(translated_record) labeled_records.append(translated_record)
return labeled_records return labeled_records

View File

@@ -12,14 +12,14 @@
## GNU General Public License for more details. ## GNU General Public License for more details.
import os, sys, shlex, tempfile, shutil, re import os, sys, shlex, tempfile, shutil, re
from datetime import timedelta
import subprocess, hashlib import subprocess, hashlib
from collections import Iterable from collections.abc import Iterable
from contextlib import contextmanager from contextlib import contextmanager
from multiprocessing import Process, Event
''' Utilities / Helpers ''' Utilities / Helpers
''' '''
@contextmanager @contextmanager
def temp_dir(quiet = True): def temp_dir(quiet = True):
''' Temp dir context manager ''' Temp dir context manager
@@ -67,11 +67,6 @@ def get_last_digit(str_to_search):
else: else:
return -1 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: class FSHelper:
''' File System ops helper ''' File System ops helper
''' '''
@@ -222,3 +217,41 @@ class UniquePartialMatchList(list):
either "equals to element" or "contained by exactly one element" either "equals to element" or "contained by exactly one element"
''' '''
return True if self.find(partialMatch) else False 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( setup(
name='mktxp', name='mktxp',
version='0.20', version='0.21',
url='https://github.com/akpw/mktxp', url='https://github.com/akpw/mktxp',
@@ -43,7 +43,13 @@ setup(
keywords = 'Mikrotik RouterOS Prometheus Exporter', 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', test_suite = 'tests.mktxp_test_suite',