From d25fbf63abb5d9bb06c17aef4f6febd875909857 Mon Sep 17 00:00:00 2001 From: Runar Borge Date: Wed, 2 Dec 2020 00:16:40 +0100 Subject: T3103: Extended vyos.frr without multiline regex using multiline regexes are quite hard to "read" and are really easy to mess up, this commit adds a new more pythonic implementation of the library that do not need any multiline regexes. --- python/vyos/frr.py | 182 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) (limited to 'python') diff --git a/python/vyos/frr.py b/python/vyos/frr.py index 3fc75bbdf..3bab64301 100644 --- a/python/vyos/frr.py +++ b/python/vyos/frr.py @@ -68,6 +68,9 @@ Apply the new configuration: import tempfile import re from vyos import util +import logging +LOG = logging.getLogger(__name__) + _frr_daemons = ['zebra', 'bgpd', 'fabricd', 'isisd', 'ospf6d', 'ospfd', 'pbrd', 'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd'] @@ -250,6 +253,7 @@ def _replace_section(config, replacement, replace_re, before_re): return: modified configuration as a text file """ + # DEPRECATED, this is replaced by a new implementation # Check if block is configured, remove the existing instance else add a new one if re.findall(replace_re, config, flags=re.MULTILINE | re.DOTALL): # Section is in the configration, replace it @@ -281,8 +285,186 @@ def replace_section(config, replacement, from_re, to_re=r'!', before_re=r'line v startline and endline tags will be automatically added to the resulting from_re/to_re and before_re regex'es """ + # DEPRECATED, this is replaced by a new implementation return _replace_section(config, replacement, replace_re=rf'^{from_re}$.*?^{to_re}$', before_re=rf'^({before_re})$') def remove_section(config, from_re, to_re='!'): + # DEPRECATED, this is replaced by a new implementation return _replace_section(config, '', replace_re=rf'^{from_re}$.*?^{to_re}$', before_re=None) + + +def _find_first_block(config, start_pattern, stop_pattern, start_at=0): + '''Find start and stop line numbers for a config block + config: (list) A list conaining the configuration that is searched + start_pattern: (raw-str) The pattern searched for a a start of block tag + stop_pattern: (raw-str) The pattern searched for to signify the end of the block + start_at: (int) The index to start searching at in the + + Returns: + None: No complete block could be found + set(int, int): A complete block found between the line numbers returned in the set + + The object is searched from the start for the regex until the first match is found. + On a successful match it continues the search for the regex until it is found. + After a successful run a set is returned containing the start and stop line numbers. + ''' + LOG.debug(f'_find_first_block: find start={repr(start_pattern)} stop={repr(stop_pattern)} start_at={start_at}') + _start = None + for i, element in enumerate(config[start_at:], start=start_at): + # LOG.debug(f'_find_first_block: running line {i:3} "{element}"') + if not _start: + if not re.match(start_pattern, element): + LOG.debug(f'_find_first_block: no match {i:3} "{element}"') + continue + _start = i + LOG.debug(f'_find_first_block: Found start {i:3} "{element}"') + continue + + if not re.match(stop_pattern, element): + LOG.debug(f'_find_first_block: no match {i:3} "{element}"') + continue + + LOG.debug(f'_find_first_block: Found stop {i:3} "{element}"') + return (_start, i) + + LOG.debug('_find_first_block: exit start={repr(start_pattern)} stop={repr(stop_pattern)} start_at={start_at}') + return None + + +def _find_first_element(config, pattern, start_at=0): + '''Find the first element that matches the current pattern in config + config: (list) A list containing the configuration that is searched + start_pattern: (raw-str) The pattern searched for + start_at: (int) The index to start searching at in the + + return: Line index of the line containing the searched pattern + + TODO: for now it returns -1 on a no-match because 0 also returns as False + TODO: that means that we can not use False matching to tell if its + ''' + LOG.debug(f'_find_first_element: find start="{pattern}" start_at={start_at}') + for i, element in enumerate(config[start_at:], start=0): + if re.match(pattern + '$', element): + LOG.debug(f'_find_first_element: Found stop {i:3} "{element}"') + return i + LOG.debug(f'_find_first_element: no match {i:3} "{element}"') + LOG.debug(f'_find_first_element: Did not find any match, exiting') + return -1 + + +def _find_elements(config, pattern, start_at=0): + '''Find all instances of pattern and return a list containing all element indexes + config: (list) A list containing the configuration that is searched + start_pattern: (raw-str) The pattern searched for + start_at: (int) The index to start searching at in the + + return: A list of line indexes containing the searched pattern + TODO: refactor this to return a generator instead + ''' + return [i for i, element in enumerate(config[start_at:], start=0) if re.match(pattern + '$', element)] + + +class FRRConfig: + '''Main FRR Configuration manipulation object + Using this object the user could load, manipulate and commit the configuration to FRR + ''' + def __init__(self, config=[]): + self.imported_config = '' + + if isinstance(config, list): + self.config = config.copy() + self.original_config = config.copy() + elif isinstance(config, str): + self.config = config.split('\n') + self.original_config = self.config.copy() + else: + raise ValueError( + 'The config element needs to be a string or list type object') + + def load_configuration(self, daemon=None): + '''Load the running configuration from FRR into the config object + daemon: str with name of the FRR Daemon to load configuration from or + None to load the consolidated config + + Using this overwrites the current loaded config objects and replaces the original loaded config + ''' + self.imported_config = get_configuration(daemon=daemon) + LOG.debug(f'load_configuration: Configuration loaded from FRR: {self.imported_config}') + self.original_config = self.imported_config.split('\n') + self.config = self.original_config.copy() + return + + def test_configuration(self): + '''Test the current configuration against FRR + This will exception if FRR failes to load the current configuration object + ''' + LOG.debug('test_configation: Testing configuration') + mark_configuration('\n'.join(self.config)) + + def commit_configuration(self, daemon=None): + '''Commit the current configuration to FRR + daemon: str with name of the FRR daemon to commit to or + None to use the consolidated config + ''' + LOG.debug('commit_configuration: Commiting configuration') + reload_configuration('\n'.join(self.config), daemon=daemon) + + def modify_section(self, start_pattern, replacement=[], stop_pattern=r'\S+', remove_stop_mark=False, count=0): + if isinstance(replacement, str): + replacement = replacement.split('\n') + elif not isinstance(replacement, list): + return ValueError("The replacement element needs to be a string or list type object") + LOG.debug(f'modify_section: starting search for {repr(start_pattern)} until {repr(stop_pattern)}') + + _count = 0 + _next_start = 0 + while True: + if count and count <= _count: + # Break out of the loop after specified amount of matches + LOG.debug(f'modify_section: reached limit ({_count}), exiting loop at line {_next_start}') + break + # While searching, always assume that the user wants to search for the exact pattern he entered + # To be more specific the user needs a override, eg. a "pattern.*" + _w = _find_first_block( + self.config, start_pattern+'$', stop_pattern, start_at=_next_start) + if not _w: + # Reached the end, no more elements to remove + LOG.debug(f'modify_section: No more config sections found, exiting') + break + start_element, end_element = _w + LOG.debug(f'modify_section: found match between {start_element} and {end_element}') + for i, e in enumerate(self.config[start_element:end_element+1 if remove_stop_mark else end_element], + start=start_element): + LOG.debug(f'modify_section: remove {i:3} {e}') + del self.config[start_element:end_element + + 1 if remove_stop_mark else end_element] + if replacement: + # Append the replacement config at the current position + for i, e in enumerate(replacement, start=start_element): + LOG.debug(f'modify_section: add {i:3} {e}') + self.config[start_element:start_element] = replacement + _count += 1 + _next_start = start_element + len(replacement) + + return _count + + def add_before(self, before_pattern, addition): + '''Add config block before this element in the configuration''' + if isinstance(addition, str): + addition = addition.split('\n') + elif not isinstance(addition, list): + return ValueError("The replacement element needs to be a string or list type object") + + start = _find_first_element(self.config, before_pattern) + if start < 0: + return False + + self.config[start:start] = addition + return True + + def __str__(self): + return '\n'.join(self.config) + + def __repr__(self): + return f'frr({repr(str(self))})' -- cgit v1.2.3