summaryrefslogtreecommitdiff
path: root/python/vyos/frr.py
diff options
context:
space:
mode:
authorRunar Borge <runar@borge.nu>2020-12-02 00:16:40 +0100
committerRunar Borge <runar@borge.nu>2020-12-02 00:16:40 +0100
commitd25fbf63abb5d9bb06c17aef4f6febd875909857 (patch)
treebcfd42fecba76074413657e0d34ba09f8b4447cf /python/vyos/frr.py
parenta37c79216f6027bed7558223e3c9863c8a571a97 (diff)
downloadvyos-1x-d25fbf63abb5d9bb06c17aef4f6febd875909857.tar.gz
vyos-1x-d25fbf63abb5d9bb06c17aef4f6febd875909857.zip
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.
Diffstat (limited to 'python/vyos/frr.py')
-rw-r--r--python/vyos/frr.py182
1 files changed, 182 insertions, 0 deletions
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 <config>
+
+ 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 <config> is searched from the start for the regex <start_pattern> until the first match is found.
+ On a successful match it continues the search for the regex <stop_pattern> 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 <config>
+
+ 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 <config>
+
+ 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))})'