Skip to content

Commit

Permalink
Network connection handling optimizations
Browse files Browse the repository at this point in the history
  • Loading branch information
akpw committed Jun 3, 2024
1 parent b6fb7b3 commit 08e7dbd
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 64 deletions.
15 changes: 9 additions & 6 deletions mktxp/collector/base_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

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

Expand All @@ -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)
Expand Down
13 changes: 5 additions & 8 deletions mktxp/datasource/base_ds.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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


Expand Down
15 changes: 9 additions & 6 deletions mktxp/datasource/neighbor_ds.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
41 changes: 26 additions & 15 deletions mktxp/flow/router_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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):
Expand Down Expand Up @@ -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



Expand Down
43 changes: 24 additions & 19 deletions mktxp/flow/router_entries_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,25 +22,14 @@ 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
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
Expand All @@ -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
69 changes: 59 additions & 10 deletions mktxp/flow/router_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -128,21 +133,65 @@ 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):
self._dhcp_records = {}
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


0 comments on commit 08e7dbd

Please sign in to comment.