diff options
-rw-r--r-- | python/vyos/frr.py | 569 | ||||
-rw-r--r-- | python/vyos/frrender.py | 1 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_interfaces_bonding.py | 2 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_interfaces_ethernet.py | 2 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_protocols_bgp.py | 2 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_protocols_isis.py | 2 | ||||
-rw-r--r-- | smoketest/scripts/cli/test_protocols_openfabric.py | 2 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_protocols_segment-routing.py | 2 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_system_ipv6.py | 2 |
9 files changed, 8 insertions, 576 deletions
diff --git a/python/vyos/frr.py b/python/vyos/frr.py deleted file mode 100644 index 67279a6f7..000000000 --- a/python/vyos/frr.py +++ /dev/null @@ -1,569 +0,0 @@ -# Copyright 2020-2024 VyOS maintainers and contributors <maintainers@vyos.io> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this library. If not, see <http://www.gnu.org/licenses/>. - -r""" -A Library for interracting with the FRR daemon suite. -It supports simple configuration manipulation and loading using the official tools -supplied with FRR (vtysh and frr-reload) - -All configuration management and manipulation is done using strings and regex. - - -Example Usage -##### - -# Reading configuration from frr: -``` ->>> original_config = get_configuration() ->>> repr(original_config) -'!\nfrr version 7.3.1\nfrr defaults traditional\nhostname debian\n...... -``` - - -# Modify a configuration section: -``` ->>> new_bgp_section = 'router bgp 65000\n neighbor 192.0.2.1 remote-as 65000\n' ->>> modified_config = replace_section(original_config, new_bgp_section, replace_re=r'router bgp \d+') ->>> repr(modified_config) -'............router bgp 65000\n neighbor 192.0.2.1 remote-as 65000\n...........' -``` - -Remove a configuration section: -``` ->>> modified_config = remove_section(original_config, r'router ospf') -``` - -Test the new configuration: -``` ->>> try: ->>> mark_configuration(modified configuration) ->>> except ConfigurationNotValid as e: ->>> print('resulting configuration is not valid') ->>> sys.exit(1) -``` - -Apply the new configuration: -``` ->>> try: ->>> replace_configuration(modified_config) ->>> except CommitError as e: ->>> print('Exception while commiting the supplied configuration') ->>> print(e) ->>> exit(1) -``` -""" - -import tempfile -import re - -from vyos import ConfigError -from vyos.defaults import frr_debug_enable -from vyos.utils.process import cmd -from vyos.utils.process import popen -from vyos.utils.process import STDOUT - -import logging -from logging.handlers import SysLogHandler -import os -import sys - -LOG = logging.getLogger(__name__) -DEBUG = False - -ch = SysLogHandler(address='/dev/log') -ch2 = logging.StreamHandler(stream=sys.stdout) -LOG.addHandler(ch) -LOG.addHandler(ch2) - -babel_daemon = 'babeld' -bfd_daemon = 'bfdd' -bgp_daemon = 'bgpd' -eigrp_daemon = 'eigrpd' -isis_daemon = 'isisd' -ldpd_daemon = 'ldpd' -mgmt_daemon = 'mgmtd' -openfabric_daemon = 'fabricd' -ospf_daemon = 'ospfd' -ospf6_daemon = 'ospf6d' -pim_daemon = 'pimd' -pim6_daemon = 'pim6d' -rip_daemon = 'ripd' -ripng_daemon = 'ripngd' -static_daemon = 'staticd' -zebra_daemon = 'zebra' - -_frr_daemons = [zebra_daemon, static_daemon, bgp_daemon, ospf_daemon, ospf6_daemon, rip_daemon, ripng_daemon, mgmt_daemon, - isis_daemon, pim_daemon, pim6_daemon, ldpd_daemon, eigrp_daemon, babel_daemon, bfd_daemon, openfabric_daemon] - -path_vtysh = '/usr/bin/vtysh' -path_frr_reload = '/usr/lib/frr/frr-reload.py' -path_config = '/run/frr' - -default_add_before = r'(ip prefix-list .*|route-map .*|line vty|end)' - - -class FrrError(Exception): - pass - - -class ConfigurationNotValid(FrrError): - """ - The configuratioin supplied to vtysh is not valid - """ - pass - - -class CommitError(FrrError): - """ - Commiting the supplied configuration failed to commit by a unknown reason - see commit error and/or run mark_configuration on the specified configuration - to se error generated - - used by: reload_configuration() - """ - pass - - -class ConfigSectionNotFound(FrrError): - """ - Removal of configuration failed because it is not existing in the supplied configuration - """ - pass - -def init_debugging(): - global DEBUG - - DEBUG = os.path.exists(frr_debug_enable) - if DEBUG: - LOG.setLevel(logging.DEBUG) - -def get_configuration(daemon=None, marked=False): - """ Get current running FRR configuration - daemon: Collect only configuration for the specified FRR daemon, - supplying daemon=None retrieves the complete configuration - marked: Mark the configuration with "end" tags - - return: string containing the running configuration from frr - - """ - if daemon and daemon not in _frr_daemons: - raise ValueError(f'The specified daemon type is not supported {repr(daemon)}') - - cmd = f"{path_vtysh} -c 'show run'" - if daemon: - cmd += f' -d {daemon}' - - output, code = popen(cmd, stderr=STDOUT) - if code: - raise OSError(code, output) - - config = output.replace('\r', '') - # Remove first header lines from FRR config - config = config.split("\n", 3)[-1] - # Mark the configuration with end tags - if marked: - config = mark_configuration(config) - - return config - - -def mark_configuration(config): - """ Add end marks and Test the configuration for syntax faults - If the configuration is valid a marked version of the configuration is returned, - or else it failes with a ConfigurationNotValid Exception - - config: The configuration string to mark/test - return: The marked configuration from FRR - """ - output, code = popen(f"{path_vtysh} -m -f -", stderr=STDOUT, input=config) - - if code == 2: - raise ConfigurationNotValid(str(output)) - elif code: - raise OSError(code, output) - - config = output.replace('\r', '') - return config - - -def reload_configuration(config, daemon=None): - """ Execute frr-reload with the new configuration - This will try to reapply the supplied configuration inside FRR. - The configuration needs to be a complete configuration from the integrated config or - from a daemon. - - config: The configuration to apply - daemon: Apply the conigutaion to the specified FRR daemon, - supplying daemon=None applies to the integrated configuration - return: None - """ - if daemon and daemon not in _frr_daemons: - raise ValueError(f'The specified daemon type is not supported {repr(daemon)}') - - f = tempfile.NamedTemporaryFile('w') - f.write(config) - f.flush() - - LOG.debug(f'reload_configuration: Reloading config using temporary file: {f.name}') - cmd = f'{path_frr_reload} --reload' - if daemon: - cmd += f' --daemon {daemon}' - - if DEBUG: - cmd += f' --debug --stdout' - - cmd += f' {f.name}' - - LOG.debug(f'reload_configuration: Executing command against frr-reload: "{cmd}"') - output, code = popen(cmd, stderr=STDOUT) - f.close() - - for i, e in enumerate(output.split('\n')): - LOG.debug(f'frr-reload output: {i:3} {e}') - - if code == 1: - raise ConfigError(output) - elif code: - raise OSError(code, output) - - return output - - -def save_configuration(): - """ T3217: Save FRR configuration to /run/frr/config/frr.conf """ - return cmd(f'{path_vtysh} -n -w') - - -def execute(command): - """ Run commands inside vtysh - command: str containing commands to execute inside a vtysh session - """ - if not isinstance(command, str): - raise ValueError(f'command needs to be a string: {repr(command)}') - - cmd = f"{path_vtysh} -c '{command}'" - - output, code = popen(cmd, stderr=STDOUT) - if code: - raise OSError(code, output) - - config = output.replace('\r', '') - return config - - -def configure(lines, daemon=False): - """ run commands inside config mode vtysh - lines: list or str conaining commands to execute inside a configure session - only one command executed on each configure() - Executing commands inside a subcontext uses the list to describe the context - ex: ['router bgp 6500', 'neighbor 192.0.2.1 remote-as 65000'] - return: None - """ - if isinstance(lines, str): - lines = [lines] - elif not isinstance(lines, list): - raise ValueError('lines needs to be string or list of commands') - - if daemon and daemon not in _frr_daemons: - raise ValueError(f'The specified daemon type is not supported {repr(daemon)}') - - cmd = f'{path_vtysh}' - if daemon: - cmd += f' -d {daemon}' - - cmd += " -c 'configure terminal'" - for x in lines: - cmd += f" -c '{x}'" - - output, code = popen(cmd, stderr=STDOUT) - if code == 1: - raise ConfigurationNotValid(f'Configuration FRR failed: {repr(output)}') - elif code: - raise OSError(code, output) - - config = output.replace('\r', '') - return config - - -def _replace_section(config, replacement, replace_re, before_re): - r"""Replace a section of FRR config - config: full original configuration - replacement: replacement configuration section - replace_re: The regex to replace - example: ^router bgp \d+$.?*^!$ - this will replace everything between ^router bgp X$ and ^!$ - before_re: When replace_re is not existant, the config will be added before this tag - example: ^line vty$ - - 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 - return re.sub(replace_re, replacement, config, count=1, - flags=re.MULTILINE | re.DOTALL) - if before_re: - if not re.findall(before_re, config, flags=re.MULTILINE | re.DOTALL): - raise ConfigSectionNotFound(f"Config section {before_re} not found in config") - - # If no section is in the configuration, add it before the line vty line - return re.sub(before_re, rf'{replacement}\n\g<1>', config, count=1, - flags=re.MULTILINE | re.DOTALL) - - raise ConfigSectionNotFound(f"Config section {replacement} not found in config") - - -def replace_section(config, replacement, from_re, to_re=r'!', before_re=r'line vty'): - r"""Replace a section of FRR config - config: full original configuration - replacement: replacement configuration section - from_re: Regex for the start of section matching - example: 'router bgp \d+' - to_re: Regex for stop of section matching - default: '!' - example: '!' or 'end' - before_re: When from_re/to_re does not return a match, the config will - be added before this tag - default: ^line vty$ - - 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') - - if config: - LOG.debug(f'__init__: frr library initiated with initial config') - for i, e in enumerate(self.config): - LOG.debug(f'__init__: initial {i:3} {e}') - - 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 - ''' - init_debugging() - - self.imported_config = get_configuration(daemon=daemon) - if daemon: - LOG.debug(f'load_configuration: Configuration loaded from FRR daemon {daemon}') - else: - LOG.debug(f'load_configuration: Configuration loaded from FRR integrated config') - - self.original_config = self.imported_config.split('\n') - self.config = self.original_config.copy() - - for i, e in enumerate(self.imported_config.split('\n')): - LOG.debug(f'load_configuration: loaded {i:3} {e}') - 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. - - Configuration is automatically saved after apply - ''' - LOG.debug('commit_configuration: Commiting configuration') - for i, e in enumerate(self.config): - LOG.debug(f'commit_configuration: new_config {i:3} {e}') - - # https://github.com/FRRouting/frr/issues/10132 - # https://github.com/FRRouting/frr/issues/10133 - count = 0 - count_max = 5 - emsg = '' - while count < count_max: - count += 1 - try: - reload_configuration('\n'.join(self.config), daemon=daemon) - break - except ConfigError as e: - emsg = str(e) - except: - # we just need to re-try the commit of the configuration - # for the listed FRR issues above - pass - if count >= count_max: - if emsg: - raise ConfigError(emsg) - raise ConfigurationNotValid(f'Config commit retry counter ({count_max}) exceeded for {daemon} daemon!') - - # Save configuration to /run/frr/config/frr.conf - save_configuration() - - - 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 - for i, e in enumerate(addition, start=start): - LOG.debug(f'add_before: add {i:3} {e}') - self.config[start:start] = addition - return True - - def __str__(self): - return '\n'.join(self.config) - - def __repr__(self): - return f'frr({repr(str(self))})' diff --git a/python/vyos/frrender.py b/python/vyos/frrender.py index 7a0b661a3..ead893ff9 100644 --- a/python/vyos/frrender.py +++ b/python/vyos/frrender.py @@ -39,6 +39,7 @@ frr_protocols = ['babel', 'bfd', 'bgp', 'eigrp', 'isis', 'mpls', 'nhrp', bgp_daemon = 'bgpd' isis_daemon = 'isisd' mgmt_daemon = 'mgmtd' +openfabric_daemon = 'fabricd' pim_daemon = 'pimd' zebra_daemon = 'zebra' diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py index 418744712..735e4f3c5 100755 --- a/smoketest/scripts/cli/test_interfaces_bonding.py +++ b/smoketest/scripts/cli/test_interfaces_bonding.py @@ -24,7 +24,7 @@ from vyos.ifconfig.interface import Interface from vyos.configsession import ConfigSessionError from vyos.utils.network import get_interface_config from vyos.utils.file import read_file -from vyos.frr import mgmt_daemon +from vyos.frrender import mgmt_daemon class BondingInterfaceTest(BasicInterfaceTest.TestCase): @classmethod diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py index 218fa0759..c02ca613b 100755 --- a/smoketest/scripts/cli/test_interfaces_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_ethernet.py @@ -27,7 +27,7 @@ from netifaces import ifaddresses from base_interfaces_test import BasicInterfaceTest from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section -from vyos.frr import mgmt_daemon +from vyos.frrender import mgmt_daemon from vyos.utils.process import cmd from vyos.utils.process import popen from vyos.utils.file import read_file diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index 6eeac37a7..1b6c30dfe 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -25,7 +25,7 @@ from vyos.configsession import ConfigSessionError from vyos.template import is_ipv6 from vyos.utils.process import process_named_running from vyos.utils.process import cmd -from vyos.frr import bgp_daemon +from vyos.frrender import bgp_daemon PROCESS_NAME = 'bgpd' ASN = '64512' diff --git a/smoketest/scripts/cli/test_protocols_isis.py b/smoketest/scripts/cli/test_protocols_isis.py index 5b86dd53a..2fddbfeba 100755 --- a/smoketest/scripts/cli/test_protocols_isis.py +++ b/smoketest/scripts/cli/test_protocols_isis.py @@ -20,7 +20,7 @@ from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section from vyos.utils.process import process_named_running -from vyos.frr import isis_daemon +from vyos.frrender import isis_daemon PROCESS_NAME = 'isisd' base_path = ['protocols', 'isis'] diff --git a/smoketest/scripts/cli/test_protocols_openfabric.py b/smoketest/scripts/cli/test_protocols_openfabric.py index 889cba135..3e99656ec 100644 --- a/smoketest/scripts/cli/test_protocols_openfabric.py +++ b/smoketest/scripts/cli/test_protocols_openfabric.py @@ -19,7 +19,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.utils.process import process_named_running -from vyos.frr import openfabric_daemon +from vyos.frrender import openfabric_daemon PROCESS_NAME = 'fabricd' base_path = ['protocols', 'openfabric'] diff --git a/smoketest/scripts/cli/test_protocols_segment-routing.py b/smoketest/scripts/cli/test_protocols_segment-routing.py index eb563db93..8905d5e45 100755 --- a/smoketest/scripts/cli/test_protocols_segment-routing.py +++ b/smoketest/scripts/cli/test_protocols_segment-routing.py @@ -20,7 +20,7 @@ from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section -from vyos.frr import zebra_daemon +from vyos.frrender import zebra_daemon from vyos.utils.process import process_named_running from vyos.utils.system import sysctl_read diff --git a/smoketest/scripts/cli/test_system_ipv6.py b/smoketest/scripts/cli/test_system_ipv6.py index be9751c4d..ebf620204 100755 --- a/smoketest/scripts/cli/test_system_ipv6.py +++ b/smoketest/scripts/cli/test_system_ipv6.py @@ -152,4 +152,4 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase): self.assertNotIn(f'no ipv6 nht resolve-via-default', frrconfig) if __name__ == '__main__': - unittest.main(verbosity=2, failfast=True) + unittest.main(verbosity=2) |