diff --git a/data/templates/frr/daemons.frr.tmpl b/data/templates/frr/daemons.frr.tmpl index fe2610724..5057bb937 100644 --- a/data/templates/frr/daemons.frr.tmpl +++ b/data/templates/frr/daemons.frr.tmpl @@ -1,57 +1,112 @@ -zebra=yes +# +# The watchfrr, zebra, mgmtd and staticd daemons are always started. +# +# Note: The following FRR-services must be kept disabled because they are replaced by other packages in VyOS: +# +# pimd Replaced by package igmpproxy. +# nhrpd Replaced by package opennhrp. +# pbrd Replaced by PBR in nftables. +# vrrpd Replaced by package keepalived. +# +# And these must be disabled aswell since they are currently missing a VyOS CLI: +# +# eigrp +# sharpd +# fabricd +# pathd +# + +#zebra=yes +#mgmtd=yes +#staticd=yes bgpd=yes ospfd=yes ospf6d=yes ripd=yes ripngd=yes isisd=yes pimd=no pim6d=yes ldpd=yes nhrpd=no -eigrpd=yes +eigrpd=no babeld=yes sharpd=no pbrd=no bfdd=yes -staticd=yes +fabricd=no +vrrpd=no +pathd=no -vtysh_enable=yes -zebra_options=" --daemon -A 127.0.0.1 -s 90000000 -{%- if irdp is defined %} -M irdp{% endif -%} -{%- if snmp is defined and snmp.zebra is defined %} -M snmp{% endif -%} -" -bgpd_options=" --daemon -A 127.0.0.1 -M rpki -{%- if bmp is defined %} -M bmp{% endif -%} -{%- if snmp is defined and snmp.bgpd is defined %} -M snmp{% endif -%} -" -ospfd_options=" --daemon -A 127.0.0.1 -{%- if snmp is defined and snmp.ospfd is defined %} -M snmp{% endif -%} -" -ospf6d_options=" --daemon -A ::1 -{%- if snmp is defined and snmp.ospf6d is defined %} -M snmp{% endif -%} -" -ripd_options=" --daemon -A 127.0.0.1 -{%- if snmp is defined and snmp.ripd is defined %} -M snmp{% endif -%} -" -ripngd_options=" --daemon -A ::1" -isisd_options=" --daemon -A 127.0.0.1 -{%- if snmp is defined and snmp.isisd is defined %} -M snmp{% endif -%} -" -pimd_options=" --daemon -A 127.0.0.1" -pim6d_options=" --daemon -A ::1" -ldpd_options=" --daemon -A 127.0.0.1 -{%- if snmp is defined and snmp.ldpd is defined %} -M snmp{% endif -%} -" -mgmtd_options=" --daemon -A 127.0.0.1" -nhrpd_options=" --daemon -A 127.0.0.1" -eigrpd_options=" --daemon -A 127.0.0.1" -babeld_options=" --daemon -A 127.0.0.1" -sharpd_options=" --daemon -A 127.0.0.1" -pbrd_options=" --daemon -A 127.0.0.1" -staticd_options=" --daemon -A 127.0.0.1" -bfdd_options=" --daemon -A 127.0.0.1" +# +# Define defaults for all services even those who shall be kept disabled. +# + +zebra_options=" --daemon -A 127.0.0.1 -s 90000000{{ ' -M snmp' if snmp.zebra is vyos_defined }}{{ ' -M irdp' if irdp is vyos_defined }}" +mgmtd_options=" --daemon -A 127.0.0.1" +staticd_options="--daemon -A 127.0.0.1" +bgpd_options=" --daemon -A 127.0.0.1 -M rpki{{ ' -M snmp' if snmp.bgpd is vyos_defined }}{{ ' -M bmp' if bmp is vyos_defined }}" +ospfd_options=" --daemon -A 127.0.0.1{{ ' -M snmp' if snmp.ospfd is vyos_defined }}" +ospf6d_options=" --daemon -A ::1{{ ' -M snmp' if snmp.ospf6d is vyos_defined }}" +ripd_options=" --daemon -A 127.0.0.1{{ ' -M snmp' if snmp.ripd is vyos_defined }}" +ripngd_options=" --daemon -A ::1" +isisd_options=" --daemon -A 127.0.0.1{{ ' -M snmp' if snmp.isisd is vyos_defined }}" +pimd_options=" --daemon -A 127.0.0.1" +pim6d_options=" --daemon -A ::1" +ldpd_options=" --daemon -A 127.0.0.1{{ ' -M snmp' if snmp.ldpd is vyos_defined }}" +nhrpd_options=" --daemon -A 127.0.0.1" +eigrpd_options=" --daemon -A 127.0.0.1" +babeld_options=" --daemon -A 127.0.0.1" +sharpd_options=" --daemon -A 127.0.0.1" +pbrd_options=" --daemon -A 127.0.0.1" +bfdd_options=" --daemon -A 127.0.0.1" +fabricd_options="--daemon -A 127.0.0.1" +vrrpd_options=" --daemon -A 127.0.0.1" +pathd_options=" --daemon -A 127.0.0.1" + +#frr_global_options="" + +#zebra_wrap="" +#mgmtd_wrap="" +#staticd_wrap="" +#bgpd_wrap="" +#ospfd_wrap="" +#ospf6d_wrap="" +#ripd_wrap="" +#ripngd_wrap="" +#isisd_wrap="" +#pimd_wrap="" +#pim6d_wrap="" +#ldpd_wrap="" +#nhrpd_wrap="" +#eigrpd_wrap="" +#babeld_wrap="" +#sharpd_wrap="" +#pbrd_wrap="" +#bfdd_wrap="" +#fabricd_wrap="" +#vrrpd_wrap="" +#pathd_wrap="" + +#all_wrap="" +# +# Other options. +# +# For more information see: +# https://github.com/FRRouting/frr/blob/stable/9.0/tools/etc/frr/daemons +# https://docs.frrouting.org/en/stable-9.0/setup.html +# + +vtysh_enable=yes watchfrr_enable=no valgrind_enable=no +#watchfrr_options="" + +frr_profile="traditional" + +#MAX_FDS=1024 + +#FRR_NO_ROOT="yes" + diff --git a/op-mode-definitions/restart-frr.xml.in b/op-mode-definitions/restart-frr.xml.in index 4572858b5..2c9d4b1cc 100644 --- a/op-mode-definitions/restart-frr.xml.in +++ b/op-mode-definitions/restart-frr.xml.in @@ -1,79 +1,85 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="restart"> <children> <leafNode name="all"> <properties> <help>Restart all routing daemons</help> </properties> <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart</command> </leafNode> - <leafNode name="bfd"> - <properties> - <help>Restart Bidirectional Forwarding Detection (BFD) daemon</help> - </properties> - <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon bfdd</command> - </leafNode> - <leafNode name="bgp"> + <leafNode name="zebra"> <properties> - <help>Restart Border Gateway Protocol (BGP) routing daemon</help> + <help>Restart Routing Information Base (RIB) IP manager daemon</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon bgpd</command> + <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon zebra</command> </leafNode> - <leafNode name="isis"> + <leafNode name="static"> <properties> - <help>Restart Intermediate System to Intermediate System (IS-IS) routing daemon</help> + <help>Restart static routing daemon</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon isisd</command> + <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon staticd</command> </leafNode> - <leafNode name="ldp"> + <leafNode name="bgp"> <properties> - <help>Restart the Label Distribution Protocol (LDP) daemon</help> + <help>Restart Border Gateway Protocol (BGP) routing daemon</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon ldpd</command> + <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon bgpd</command> </leafNode> <leafNode name="ospf"> <properties> <help>Restart Open Shortest Path First (OSPF) routing daemon</help> </properties> <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon ospfd</command> </leafNode> <leafNode name="ospfv3"> <properties> <help>Restart IPv6 Open Shortest Path First (OSPFv3) routing daemon</help> </properties> <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon ospf6d</command> </leafNode> <leafNode name="rip"> <properties> <help>Restart Routing Information Protocol (RIP) routing daemon</help> </properties> <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon ripd</command> </leafNode> <leafNode name="ripng"> <properties> - <help>Restart Routing Information Protocol NG (RIPng) routing daemon</help> + <help>Restart IPv6 Routing Information Protocol (RIPng) routing daemon</help> </properties> <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon ripngd</command> </leafNode> - <leafNode name="static"> + <leafNode name="isis"> <properties> - <help>Restart static routing daemon</help> + <help>Restart Intermediate System to Intermediate System (IS-IS) routing daemon</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon staticd</command> + <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon isisd</command> </leafNode> - <leafNode name="zebra"> + <leafNode name="pim6"> <properties> - <help>Restart Routing Information Base (RIB) manager daemon</help> + <help>Restart IPv6 Protocol Independent Multicast (PIM) daemon</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon zebra</command> + <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon pim6d</command> + </leafNode> + <leafNode name="ldp"> + <properties> + <help>Restart Label Distribution Protocol (LDP) daemon used by MPLS</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon ldpd</command> </leafNode> <leafNode name="babel"> <properties> <help>Restart Babel routing daemon</help> </properties> <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon babeld</command> </leafNode> + <leafNode name="bfd"> + <properties> + <help>Restart Bidirectional Forwarding Detection (BFD) daemon</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon bfdd</command> + </leafNode> </children> </node> </interfaceDefinition> diff --git a/python/vyos/frr.py b/python/vyos/frr.py index 9c9e50ff7..ad5c207f5 100644 --- a/python/vyos/frr.py +++ b/python/vyos/frr.py @@ -1,547 +1,550 @@ # 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.utils.permission import chown 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) -_frr_daemons = ['zebra', 'bgpd', 'fabricd', 'isisd', 'ospf6d', 'ospfd', 'pbrd', - 'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd', - 'bfdd', 'eigrpd', 'babeld' ,'pim6d'] +# Full list of FRR 9.0/stable daemons for reference +#_frr_daemons = ['zebra', 'staticd', 'bgpd', 'ospfd', 'ospf6d', 'ripd', 'ripngd', +# 'isisd', 'pim6d', 'ldpd', 'eigrpd', 'babeld', 'sharpd', 'bfdd', +# 'fabricd', 'pathd'] +_frr_daemons = ['zebra', 'staticd', 'bgpd', 'ospfd', 'ospf6d', 'ripd', 'ripngd', + 'isisd', 'pim6d', 'ldpd', 'babeld', 'bfdd'] 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('/tmp/vyos.frr.debug') 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 CommitError('FRR configuration failed while running commit. Please ' \ 'enable debugging to examine logs.\n\n\n' \ 'To enable debugging run: "touch /tmp/vyos.frr.debug" ' \ 'and "sudo systemctl stop vyos-configd"') 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 while count < count_max: count += 1 try: reload_configuration('\n'.join(self.config), daemon=daemon) break except: # we just need to re-try the commit of the configuration # for the listed FRR issues above pass if count >= count_max: raise ConfigurationNotValid(f'Config commit retry counter ({count_max}) exceeded for {daemon} dameon!') # 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/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 7882f8510..d2ed5414f 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -1,274 +1,273 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2021 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from sys import exit from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.configverify import verify_vrf from vyos.snmpv3_hashgen import plaintext_to_md5 from vyos.snmpv3_hashgen import plaintext_to_sha1 from vyos.snmpv3_hashgen import random from vyos.template import render from vyos.utils.process import call from vyos.utils.permission import chmod_755 from vyos.utils.dict import dict_search from vyos.utils.network import is_addr_assigned from vyos.version import get_version_data from vyos import ConfigError from vyos import airbag airbag.enable() config_file_client = r'/etc/snmp/snmp.conf' config_file_daemon = r'/etc/snmp/snmpd.conf' config_file_access = r'/usr/share/snmp/snmpd.conf' config_file_user = r'/var/lib/snmp/snmpd.conf' systemd_override = r'/run/systemd/system/snmpd.service.d/override.conf' systemd_service = 'snmpd.service' def get_config(config=None): if config: conf = config else: conf = Config() base = ['service', 'snmp'] snmp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) if not conf.exists(base): snmp.update({'deleted' : ''}) if conf.exists(['service', 'lldp', 'snmp', 'enable']): snmp.update({'lldp_snmp' : ''}) if 'deleted' in snmp: return snmp version_data = get_version_data() snmp['version'] = version_data['version'] # create an internal snmpv3 user of the form 'vyosxxxxxxxxxxxxxxxx' snmp['vyos_user'] = 'vyos' + random(8) snmp['vyos_user_pass'] = random(16) # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. snmp = conf.merge_defaults(snmp, recursive=True) if 'listen_address' in snmp: # Always listen on localhost if an explicit address has been configured # This is a safety measure to not end up with invalid listen addresses # that are not configured on this system. See https://vyos.dev/T850 if '127.0.0.1' not in snmp['listen_address']: tmp = {'127.0.0.1': {'port': '161'}} snmp['listen_address'] = dict_merge(tmp, snmp['listen_address']) if '::1' not in snmp['listen_address']: tmp = {'::1': {'port': '161'}} snmp['listen_address'] = dict_merge(tmp, snmp['listen_address']) return snmp def verify(snmp): if not snmp: return None if {'deleted', 'lldp_snmp'} <= set(snmp): raise ConfigError('Can not delete SNMP service, as LLDP still uses SNMP!') ### check if the configured script actually exist if 'script_extensions' in snmp and 'extension_name' in snmp['script_extensions']: for extension, extension_opt in snmp['script_extensions']['extension_name'].items(): if 'script' not in extension_opt: raise ConfigError(f'Script extension "{extension}" requires an actual script to be configured!') tmp = extension_opt['script'] if not os.path.isfile(tmp): Warning(f'script "{tmp}" does not exist!') else: chmod_755(extension_opt['script']) if 'listen_address' in snmp: for address in snmp['listen_address']: # We only wan't to configure addresses that exist on the system. # Hint the user if they don't exist if 'vrf' in snmp: vrf_name = snmp['vrf'] if not is_addr_assigned(address, vrf_name) and address not in ['::1','127.0.0.1']: raise ConfigError(f'SNMP listen address "{address}" not configured in vrf "{vrf_name}"!') elif not is_addr_assigned(address): raise ConfigError(f'SNMP listen address "{address}" not configured in default vrf!') if 'trap_target' in snmp: for trap, trap_config in snmp['trap_target'].items(): if 'community' not in trap_config: raise ConfigError(f'Trap target "{trap}" requires a community to be set!') if 'oid_enable' in snmp: Warning(f'Custom OIDs are enabled and may lead to system instability and high resource consumption') verify_vrf(snmp) # bail out early if SNMP v3 is not configured if 'v3' not in snmp: return None if 'user' in snmp['v3']: for user, user_config in snmp['v3']['user'].items(): if 'group' not in user_config: raise ConfigError(f'Group membership required for user "{user}"!') if 'plaintext_password' not in user_config['auth'] and 'encrypted_password' not in user_config['auth']: raise ConfigError(f'Must specify authentication encrypted-password or plaintext-password for user "{user}"!') if 'plaintext_password' not in user_config['privacy'] and 'encrypted_password' not in user_config['privacy']: raise ConfigError(f'Must specify privacy encrypted-password or plaintext-password for user "{user}"!') if 'group' in snmp['v3']: for group, group_config in snmp['v3']['group'].items(): if 'seclevel' not in group_config: raise ConfigError(f'Must configure "seclevel" for group "{group}"!') if 'view' not in group_config: raise ConfigError(f'Must configure "view" for group "{group}"!') # Check if 'view' exists view = group_config['view'] if 'view' not in snmp['v3'] or view not in snmp['v3']['view']: raise ConfigError(f'You must create view "{view}" first!') if 'view' in snmp['v3']: for view, view_config in snmp['v3']['view'].items(): if 'oid' not in view_config: raise ConfigError(f'Must configure an "oid" for view "{view}"!') if 'trap_target' in snmp['v3']: for trap, trap_config in snmp['v3']['trap_target'].items(): if 'plaintext_password' not in trap_config['auth'] and 'encrypted_password' not in trap_config['auth']: raise ConfigError(f'Must specify one of authentication encrypted-password or plaintext-password for trap "{trap}"!') if {'plaintext_password', 'encrypted_password'} <= set(trap_config['auth']): raise ConfigError(f'Can not specify both authentication encrypted-password and plaintext-password for trap "{trap}"!') if 'plaintext_password' not in trap_config['privacy'] and 'encrypted_password' not in trap_config['privacy']: raise ConfigError(f'Must specify one of privacy encrypted-password or plaintext-password for trap "{trap}"!') if {'plaintext_password', 'encrypted_password'} <= set(trap_config['privacy']): raise ConfigError(f'Can not specify both privacy encrypted-password and plaintext-password for trap "{trap}"!') if 'type' not in trap_config: raise ConfigError('SNMP v3 trap "type" must be specified!') return None def generate(snmp): # # As we are manipulating the snmpd user database we have to stop it first! # This is even save if service is going to be removed call(f'systemctl stop {systemd_service}') # Clean config files config_files = [config_file_client, config_file_daemon, config_file_access, config_file_user, systemd_override] for file in config_files: if os.path.isfile(file): os.unlink(file) if not snmp: return None if 'v3' in snmp: # net-snmp is now regenerating the configuration file in the background # thus we need to re-open and re-read the file as the content changed. # After that we can no read the encrypted password from the config and # replace the CLI plaintext password with its encrypted version. os.environ['vyos_libexec_dir'] = '/usr/libexec/vyos' if 'user' in snmp['v3']: for user, user_config in snmp['v3']['user'].items(): if dict_search('auth.type', user_config) == 'sha': hash = plaintext_to_sha1 else: hash = plaintext_to_md5 if dict_search('auth.plaintext_password', user_config) is not None: tmp = hash(dict_search('auth.plaintext_password', user_config), dict_search('v3.engineid', snmp)) snmp['v3']['user'][user]['auth']['encrypted_password'] = tmp del snmp['v3']['user'][user]['auth']['plaintext_password'] call(f'/opt/vyatta/sbin/my_set service snmp v3 user "{user}" auth encrypted-password "{tmp}" > /dev/null') call(f'/opt/vyatta/sbin/my_delete service snmp v3 user "{user}" auth plaintext-password > /dev/null') if dict_search('privacy.plaintext_password', user_config) is not None: tmp = hash(dict_search('privacy.plaintext_password', user_config), dict_search('v3.engineid', snmp)) snmp['v3']['user'][user]['privacy']['encrypted_password'] = tmp del snmp['v3']['user'][user]['privacy']['plaintext_password'] call(f'/opt/vyatta/sbin/my_set service snmp v3 user "{user}" privacy encrypted-password "{tmp}" > /dev/null') call(f'/opt/vyatta/sbin/my_delete service snmp v3 user "{user}" privacy plaintext-password > /dev/null') # Write client config file render(config_file_client, 'snmp/etc.snmp.conf.j2', snmp) # Write server config file render(config_file_daemon, 'snmp/etc.snmpd.conf.j2', snmp) # Write access rights config file render(config_file_access, 'snmp/usr.snmpd.conf.j2', snmp) # Write access rights config file render(config_file_user, 'snmp/var.snmpd.conf.j2', snmp) # Write daemon configuration file render(systemd_override, 'snmp/override.conf.j2', snmp) return None def apply(snmp): # Always reload systemd manager configuration call('systemctl daemon-reload') if not snmp: return None # start SNMP daemon call(f'systemctl restart {systemd_service}') # Enable AgentX in FRR # This should be done for each daemon individually because common command # works only if all the daemons started with SNMP support - frr_daemons_list = [ - 'bgpd', 'ospf6d', 'ospfd', 'ripd', 'ripngd', 'isisd', 'ldpd', 'zebra' - ] + # Following daemons from FRR 9.0/stable have SNMP module compiled in VyOS + frr_daemons_list = ['zebra', 'bgpd', 'ospf6d', 'ospfd', 'ripd', 'isisd', 'ldpd'] for frr_daemon in frr_daemons_list: call( f'vtysh -c "configure terminal" -d {frr_daemon} -c "agentx" >/dev/null' ) return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py index 5cce377eb..820a3846c 100755 --- a/src/op_mode/restart_frr.py +++ b/src/op_mode/restart_frr.py @@ -1,181 +1,183 @@ #!/usr/bin/env python3 # # Copyright (C) 2019-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os import argparse import logging import psutil from logging.handlers import SysLogHandler from shutil import rmtree from vyos.base import Warning from vyos.utils.io import ask_yes_no from vyos.utils.file import makedir from vyos.utils.process import call from vyos.utils.process import process_named_running # some default values watchfrr = '/usr/lib/frr/watchfrr.sh' vtysh = '/usr/bin/vtysh' frrconfig_tmp = '/tmp/frr_restart' # configure logging logger = logging.getLogger(__name__) logs_handler = SysLogHandler('/dev/log') logs_handler.setFormatter(logging.Formatter('%(filename)s: %(message)s')) logger.addHandler(logs_handler) logger.setLevel(logging.INFO) # check if it is safe to restart FRR def _check_safety(): try: # print warning if not ask_yes_no('WARNING: This is a potentially unsafe function!\n' \ 'You may lose the connection to the router or active configuration after\n' \ 'running this command. Use it at your own risk!\n\n' 'Continue?'): return False # check if another restart process already running if len([process for process in psutil.process_iter(attrs=['pid', 'name', 'cmdline']) if 'python' in process.info['name'] and 'restart_frr.py' in process.info['cmdline'][1]]) > 1: message = 'Another restart_frr.py process is already running!' logger.error(message) if not ask_yes_no(f'\n{message} It is unsafe to continue.\n\n' \ 'Do you want to process anyway?'): return False # check if watchfrr.sh is running tmp = os.path.basename(watchfrr) if process_named_running(tmp): message = f'Another {tmp} process is already running.' logger.error(message) if not ask_yes_no(f'{message} It is unsafe to continue.\n\n' \ 'Do you want to process anyway?'): return False # check if vtysh is running if process_named_running('vtysh'): message = 'vtysh process is executed by another task.' logger.error(message) if not ask_yes_no(f'{message} It is unsafe to continue.\n\n' \ 'Do you want to process anyway?'): return False # check if temporary directory exists if os.path.exists(frrconfig_tmp): message = f'Temporary directory "{frrconfig_tmp}" already exists!' logger.error(message) if not ask_yes_no(f'{message} It is unsafe to continue.\n\n' \ 'Do you want to process anyway?'): return False except: logger.error("Something goes wrong in _check_safety()") return False # return True if all check was passed or user confirmed to ignore they results return True # write active config to file def _write_config(): # create temporary directory makedir(frrconfig_tmp) # save frr.conf to it command = f'{vtysh} -n -w --config_dir {frrconfig_tmp} 2> /dev/null' return_code = call(command) if return_code != 0: logger.error(f'Failed to save active config: "{command}" returned exit code: {return_code}') return False logger.info(f'Active config saved to {frrconfig_tmp}') return True # clear and remove temporary directory def _cleanup(): if os.path.isdir(frrconfig_tmp): rmtree(frrconfig_tmp) # restart daemon def _daemon_restart(daemon): command = f'{watchfrr} restart {daemon}' return_code = call(command) if not return_code == 0: logger.error(f'Failed to restart daemon "{daemon}"!') return False # return True if restarted successfully logger.info(f'Daemon "{daemon}" restarted!') return True # reload old config def _reload_config(daemon): if daemon != '': command = f'{vtysh} -n -b --config_dir {frrconfig_tmp} -d {daemon} 2> /dev/null' else: command = f'{vtysh} -n -b --config_dir {frrconfig_tmp} 2> /dev/null' return_code = call(command) if not return_code == 0: logger.error('Failed to re-install configuration!') return False # return True if restarted successfully logger.info('Configuration re-installed successfully!') return True # define program arguments cmd_args_parser = argparse.ArgumentParser(description='restart frr daemons') cmd_args_parser.add_argument('--action', choices=['restart'], required=True, help='action to frr daemons') -cmd_args_parser.add_argument('--daemon', choices=['bfdd', 'bgpd', 'ldpd', 'ospfd', 'ospf6d', 'isisd', 'ripd', 'ripngd', 'staticd', 'zebra', 'babeld'], required=False, nargs='*', help='select single or multiple daemons') +# Full list of FRR 9.0/stable daemons for reference +#cmd_args_parser.add_argument('--daemon', choices=['zebra', 'staticd', 'bgpd', 'ospfd', 'ospf6d', 'ripd', 'ripngd', 'isisd', 'pim6d', 'ldpd', 'eigrpd', 'babeld', 'sharpd', 'bfdd', 'fabricd', 'pathd'], required=False, nargs='*', help='select single or multiple daemons') +cmd_args_parser.add_argument('--daemon', choices=['zebra', 'staticd', 'bgpd', 'ospfd', 'ospf6d', 'ripd', 'ripngd', 'isisd', 'pim6d', 'ldpd', 'babeld', 'bfdd'], required=False, nargs='*', help='select single or multiple daemons') # parse arguments cmd_args = cmd_args_parser.parse_args() # main logic # restart daemon if cmd_args.action == 'restart': # check if it is safe to restart FRR if not _check_safety(): print("\nOne of the safety checks was failed or user aborted command. Exiting.") exit(1) if not _write_config(): print("Failed to save active config") _cleanup() exit(1) # a little trick to make further commands more clear if not cmd_args.daemon: cmd_args.daemon = [''] # check all daemons if they are running if cmd_args.daemon != ['']: for daemon in cmd_args.daemon: if not process_named_running(daemon): Warning('some of listed daemons are not running!') # run command to restart daemon for daemon in cmd_args.daemon: if not _daemon_restart(daemon): print('Failed to restart daemon: {daemon}') _cleanup() exit(1) # reinstall old configuration _reload_config(daemon) # cleanup after all actions _cleanup() exit(0)