diff --git a/mktxp/collector/base_collector.py b/mktxp/collector/base_collector.py index d52036fb..40235f6f 100644 --- a/mktxp/collector/base_collector.py +++ b/mktxp/collector/base_collector.py @@ -22,8 +22,9 @@ class BaseCollector: ''' @staticmethod def info_collector(name, decription, router_records, metric_labels=None): - if metric_labels is None: - metric_labels = [] + metric_labels = metric_labels or [] + router_records = router_records or [] + BaseCollector._add_id_labels(metric_labels) collector = InfoMetricFamily(f'mktxp_{name}', decription) @@ -34,8 +35,9 @@ def info_collector(name, decription, router_records, metric_labels=None): @staticmethod def counter_collector(name, decription, router_records, metric_key, metric_labels=None): - if metric_labels is None: - metric_labels = [] + metric_labels = metric_labels or [] + router_records = router_records or [] + BaseCollector._add_id_labels(metric_labels) collector = CounterMetricFamily(f'mktxp_{name}', decription, labels=metric_labels) @@ -46,8 +48,9 @@ def counter_collector(name, decription, router_records, metric_key, metric_label @staticmethod def gauge_collector(name, decription, router_records, metric_key, metric_labels = None, add_id_labels = True): - if metric_labels is None: - metric_labels = [] + metric_labels = metric_labels or [] + router_records = router_records or [] + if add_id_labels: BaseCollector._add_id_labels(metric_labels) collector = GaugeMetricFamily(f'mktxp_{name}', decription, labels=metric_labels) diff --git a/mktxp/datasource/base_ds.py b/mktxp/datasource/base_ds.py index 32a6b424..bd48021d 100644 --- a/mktxp/datasource/base_ds.py +++ b/mktxp/datasource/base_ds.py @@ -18,12 +18,10 @@ class BaseDSProcessor: @staticmethod def trimmed_records(router_entry, *, router_records = None, metric_labels = None, add_router_id = True, translation_table = None): - if router_records is None: - router_records = [] - if metric_labels is None: - metric_labels = [] - if translation_table is None: - translation_table = {} + metric_labels = metric_labels or [] + router_records = router_records or [] + translation_table = translation_table or {} + if len(metric_labels) == 0 and len(router_records) > 0: metric_labels = [BaseDSProcessor._normalise_keys(key) for key in router_records[0].keys()] metric_labels = set(metric_labels) @@ -39,8 +37,7 @@ def trimmed_records(router_entry, *, router_records = None, metric_labels = None # translate fields if needed for key, func in translation_table.items(): translated_record[key] = func(translated_record.get(key)) - labeled_records.append(translated_record) - + labeled_records.append(translated_record) return labeled_records diff --git a/mktxp/datasource/neighbor_ds.py b/mktxp/datasource/neighbor_ds.py index 43617e9c..a8cdff90 100644 --- a/mktxp/datasource/neighbor_ds.py +++ b/mktxp/datasource/neighbor_ds.py @@ -21,9 +21,12 @@ def metric_records(router_entry, metric_labels, ipv6=False): metric_labels = metric_labels or [] router_records = [] - if ipv6: - router_records = router_entry.api_connection.router_api().get_resource(f'/ipv6/neighbor').get(status='reachable') - else: - router_records = router_entry.api_connection.router_api().get_resource(f'/ip/neighbor').get() - - return BaseDSProcessor.trimmed_records(router_entry, router_records=router_records, metric_labels=metric_labels) + try: + if ipv6: + router_records = router_entry.api_connection.router_api().get_resource(f'/ipv6/neighbor').get(status='reachable') + else: + router_records = router_entry.api_connection.router_api().get_resource(f'/ip/neighbor').get() + return BaseDSProcessor.trimmed_records(router_entry, router_records=router_records, metric_labels=metric_labels) + except Exception as exc: + print(f'Error getting Neighbors info from router {router_entry.router_name}@{router_entry.config_entry.hostname}: {exc}') + return None diff --git a/mktxp/flow/router_connection.py b/mktxp/flow/router_connection.py index 4706eb4b..c17e14ce 100644 --- a/mktxp/flow/router_connection.py +++ b/mktxp/flow/router_connection.py @@ -17,6 +17,7 @@ import collections from datetime import datetime from mktxp.cli.config.config import config_handler +import functools # Fix UTF-8 decode error # See: https://github.com/akpw/mktxp/issues/47 @@ -31,10 +32,17 @@ from routeros_api import RouterOsApiPool - class RouterAPIConnectionError(Exception): pass +def check_connected(func): + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + if not self.is_connected(): + raise RouterAPIConnectionError(f'No network connection to router: {self.router_name}@{self.config_entry.hostname}') + else: + return func(self, *args, **kwargs) + return wrapper class RouterAPIConnection: ''' Base wrapper interface for the routeros_api library @@ -61,16 +69,11 @@ def __init__(self, router_name, config_entry): self.connection.socket_timeout = config_handler.system_entry.socket_timeout self.api = None - + def is_connected(self): - if not (self.connection and self.connection.connected and self.api): - return False - try: - self.api.get_resource('/system/identity').get() + if self.connection and self.connection.connected and self.api: return True - except (socket.error, socket.timeout, Exception) as exc: - self._set_connect_state(success = False, exc = exc) - return False + return False def connect(self): connect_time = datetime.now() @@ -83,11 +86,10 @@ def connect(self): self._set_connect_state(success = True, connect_time = connect_time) except (socket.error, socket.timeout, Exception) as exc: self._set_connect_state(success = False, connect_time = connect_time, exc = exc) - #raise RouterAPIConnectionError + raise RouterAPIConnectionError(f'Failed attemp to establish network connection to router: {self.router_name}@{self.config_entry.hostname}') + @check_connected def router_api(self): - if not self.is_connected(): - self.connect() return self.api def _in_connect_timeout(self, connect_timestamp): @@ -122,9 +124,18 @@ def _set_connect_state(self, success = False, connect_time = datetime.now(), exc print(f'{connect_time.strftime("%Y-%m-%d %H:%M:%S")} Connection to router {self.router_name}@{self.config_entry.hostname} has failed: {exc}') - - - +# def is_not_connected(self): +# if not (self.connection and self.connection.connected and self.api): +# return True +# def is_connected(self): +# if self.is_not_connected(): +# return False +# try: +# self.api.get_resource('/system/identity').get() +# return True +# except (socket.error, socket.timeout, Exception) as exc: +# self._set_connect_state(success = False, exc = exc) +# return False diff --git a/mktxp/flow/router_entries_handler.py b/mktxp/flow/router_entries_handler.py index a9d6c325..4613f3a3 100644 --- a/mktxp/flow/router_entries_handler.py +++ b/mktxp/flow/router_entries_handler.py @@ -11,10 +11,9 @@ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. - from mktxp.cli.config.config import config_handler from mktxp.flow.router_entry import RouterEntry - +from mktxp.flow.router_connection import RouterAPIConnectionError class RouterEntriesHandler: ''' Handles RouterOS entries defined in MKTXP config @@ -23,12 +22,7 @@ def __init__(self): self._router_entries = {} for router_name in config_handler.registered_entries(): router_entry = RouterEntry(router_name) - if router_entry.config_entry.remote_dhcp_entry and config_handler.registered_entry(router_entry.config_entry.remote_dhcp_entry): - router_entry.dhcp_entry = RouterEntry(router_entry.config_entry.remote_dhcp_entry) - - if router_entry.config_entry.remote_capsman_entry and config_handler.registered_entry(router_entry.config_entry.remote_capsman_entry): - router_entry.capsman_entry = RouterEntry(router_entry.config_entry.remote_capsman_entry) - + RouterEntriesHandler._set_child_entries(router_entry) self._router_entries[router_name] = router_entry @property @@ -36,12 +30,6 @@ def router_entries(self): return (entry for key, entry in self._router_entries.items() if entry.config_entry.enabled) \ if self._router_entries else None - def router_entry(self, entry_name, enabled_only = False): - entry = self._router_entries.get(entry_name) - if entry and (entry.config_entry.enabled or not enabled_only): - return entry - return None - @staticmethod def router_entry(entry_name, enabled_only = False): ''' A static router entry initialiser @@ -51,10 +39,27 @@ def router_entry(entry_name, enabled_only = False): return None router_entry = RouterEntry(entry_name) - if config_entry.remote_dhcp_entry and config_handler.registered_entry(config_entry.remote_dhcp_entry): - router_entry.dhcp_entry = RouterEntry(config_entry.remote_dhcp_entry) + RouterEntriesHandler._set_child_entries(router_entry) + try: + router_entry.connect() + except RouterAPIConnectionError as exc: + print (f'{exc}') + return router_entry - if config_entry.remote_capsman_entry and config_handler.registered_entry(config_entry.remote_capsman_entry): - router_entry.capsman_entry = RouterEntry(config_entry.remote_capsman_entry) + @staticmethod + def _set_child_entries(router_entry): + if router_entry.config_entry.remote_dhcp_entry and config_handler.registered_entry(router_entry.config_entry.remote_dhcp_entry): + router_entry.dhcp_entry = RouterEntry(router_entry.config_entry.remote_dhcp_entry) + + if router_entry.config_entry.remote_capsman_entry and config_handler.registered_entry(router_entry.config_entry.remote_capsman_entry): + router_entry.capsman_entry = RouterEntry(router_entry.config_entry.remote_capsman_entry) + + +#import sys +#_, ex_value, _ = sys.exc_info() +# def router_entry(self, entry_name, enabled_only = False): +# entry = self._router_entries.get(entry_name) +# if entry and (entry.config_entry.enabled or not enabled_only): +# return entry +# return None - return router_entry diff --git a/mktxp/flow/router_entry.py b/mktxp/flow/router_entry.py index bf476dd8..10ea4631 100644 --- a/mktxp/flow/router_entry.py +++ b/mktxp/flow/router_entry.py @@ -18,7 +18,7 @@ from mktxp.flow.router_connection import RouterAPIConnection from mktxp.datasource.package_ds import PackageMetricsDataSource from mktxp.datasource.system_resource_ds import SystemResourceMetricsDataSource - +from mktxp.flow.router_connection import RouterAPIConnectionError class RouterEntryWirelessType(IntEnum): NONE = 0 @@ -33,12 +33,17 @@ class RouterEntryWirelessPackage: WIFIWAVE2_PACKAGE = 'wifiwave2' WIRELESS_PACKAGE = 'wireless' +class RouterEntryConnectionState(IntEnum): + NOT_CONNECTED = 0 + PARTIALLY_CONNECTED = 1 + CONNECTED = 2 + class RouterEntry: ''' RouterOS Entry ''' def __init__(self, router_name): self.router_name = router_name - self.config_entry = config_handler.config_entry(router_name) + self.config_entry = config_handler.config_entry(router_name) self.api_connection = RouterAPIConnection(router_name, self.config_entry) self.router_id = { MKTXPConfigKeys.ROUTERBOARD_NAME: self.router_name, @@ -128,17 +133,47 @@ def dhcp_record(self, key): return self._dhcp_records[key].record return None - def is_ready(self): - self.is_done() #flush caches, just in case - is_ready = True + def connection_status(self): + primary_connection_status = self.api_connection.is_connected() + dhcp_connection_status = self.dhcp_entry.api_connection.is_connected() + capsman_connection_status = self.capsman_entry.api_connection.is_connected() + + if primary_connection_status and dhcp_connection_status and capsman_connection_status: + return RouterEntryConnectionState.CONNECTED + + if primary_connection_status or dhcp_connection_status or capsman_connection_status: + return RouterEntryConnectionState.PARTIALLY_CONNECTED + + return RouterEntryConnectionState.NOT_CONNECTED + + def connect(self): if not self.api_connection.is_connected(): - is_ready = False - # let's get connected now - self.api_connection.connect() - if self._dhcp_entry: + try: + self.api_connection.connect() + except RouterAPIConnectionError as exc: + print (f'{exc}') + + if self._dhcp_entry and not self._dhcp_entry.api_connection.is_connected(): + try: self._dhcp_entry.api_connection.connect() - if self._capsman_entry: + except RouterAPIConnectionError as exc: + print (f'{exc}') + + if self._capsman_entry and not self._capsman_entry.api_connection.is_connected(): + try: self._capsman_entry.api_connection.connect() + except RouterAPIConnectionError as exc: + print (f'{exc}') + + def is_ready(self): + self.is_done() #flush caches, just in case + is_ready = False + + if self.connection_status() in (RouterEntryConnectionState.NOT_CONNECTED, RouterEntryConnectionState.PARTIALLY_CONNECTED): + self.connect() + if self.connection_status() in (RouterEntryConnectionState.CONNECTED, RouterEntryConnectionState.PARTIALLY_CONNECTED): + is_ready = True + return is_ready def is_done(self): @@ -146,3 +181,17 @@ def is_done(self): self._wireless_type = RouterEntryWirelessType.NONE DHCPCacheEntry = namedtuple('DHCPCacheEntry', ['type', 'record']) + + + + +# def is_connected(self): +# if self.api_connection.is_connected(): +# return True +# if self._dhcp_entry and self._dhcp_entry.api_connection.is_connected(): +# return True +# if self._capsman_entry and self._capsman_entry.api_connection.is_connected(): +# return True +# return False + +