From d48568a8e4bf3619bd80fcc5db82ad0c7314157c Mon Sep 17 00:00:00 2001 From: Runar Borge Date: Wed, 3 Jun 2020 23:05:27 +0200 Subject: T2638: FRR: New framework for configuring FRR This commit adds the python module vyos.frr that support reading and modifying the frr cofiguration. Functions get_configuration() : Collects the configuration from FRR and returns it as a string reload_configuration() : uses frr-reload.py to activate a new configuration. The configuration applied will overwrite the current configuration. mark_configuration() : Does syntax check/validation and add "end" tags to the configuration The marked configuration will be returned as a string A syntax fault will render an Exception execute() : Execute a command inside vtysh configure() : Executes a command in vtysh config mode replace_section() / _replace_section() : Adds the ability to replace a section of frr code remove_section() : Removes the specified configuration block from the config For now this supports replacing complete config sub-blocks of configuration and selecting the daemon to replace inside. This should work for most daemons, but static routing will still be an issue because this is not a separate sub-config mode --- debian/control | 1 + python/vyos/frr.py | 288 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 289 insertions(+) create mode 100644 python/vyos/frr.py diff --git a/debian/control b/debian/control index 5e14340a8..d2a1ad54c 100644 --- a/debian/control +++ b/debian/control @@ -88,6 +88,7 @@ Depends: python3, iperf, iperf3, frr, + frr-pythontools, radvd, dbus, usb-modeswitch, diff --git a/python/vyos/frr.py b/python/vyos/frr.py new file mode 100644 index 000000000..e39b6a914 --- /dev/null +++ b/python/vyos/frr.py @@ -0,0 +1,288 @@ +# Copyright 2020 VyOS maintainers and contributors +# +# 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 . + +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 util + +_frr_daemons = ['zebra', 'bgpd', 'fabricd', 'isisd', 'ospf6d', 'ospfd', 'pbrd', + 'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd'] + +path_vtysh = '/usr/bin/vtysh' +path_frr_reload = '/usr/lib/frr/frr-reload.py' + + +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 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 = util.popen(cmd, stderr=util.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 = util.popen(f"{path_vtysh} -m -f -", stderr=util.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() + + cmd = f'{path_frr_reload} --reload' + if daemon: + cmd += f' --daemon {daemon}' + cmd += f' {f.name}' + + output, code = util.popen(cmd, stderr=util.STDOUT) + f.close() + if code == 1: + raise CommitError(f'Configuration FRR failed while commiting code: {repr(output)}') + elif code: + raise OSError(code, output) + + return output + + +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 = util.popen(cmd, stderr=util.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 = util.popen(cmd, stderr=util.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 + """ + # 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 + """ + 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='!'): + return _replace_section(config, '', replace_re=rf'^{from_re}$.*?^{to_re}$', before_re=None) -- cgit v1.2.3