summaryrefslogtreecommitdiff
path: root/python/vyos/frr.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/vyos/frr.py')
-rw-r--r--python/vyos/frr.py288
1 files changed, 288 insertions, 0 deletions
diff --git a/python/vyos/frr.py b/python/vyos/frr.py
new file mode 100644
index 000000000..3fc75bbdf
--- /dev/null
+++ b/python/vyos/frr.py
@@ -0,0 +1,288 @@
+# Copyright 2020 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 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)