diff options
author | Christian Breunig <christian@breunig.cc> | 2023-11-16 15:46:23 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-16 15:46:23 +0100 |
commit | 71823db0fceee36f631484d3ff6767569e1ca825 (patch) | |
tree | 3528e9ba826be0ba71556f2393685402f44a22ec /src | |
parent | cab0d62af86ced1ded6edefb53c2e902249b160d (diff) | |
parent | b9493ce110fb1ae2090299a5bcb5d3c7fc3dfd60 (diff) | |
download | vyos-1x-71823db0fceee36f631484d3ff6767569e1ca825.tar.gz vyos-1x-71823db0fceee36f631484d3ff6767569e1ca825.zip |
Merge pull request #2489 from vyos/mergify/bp/sagitta/pr-2476
pim(6): T5733: add missing FRR related features (backport #2476)
Diffstat (limited to 'src')
-rwxr-xr-x | src/conf_mode/protocols_igmp.py | 140 | ||||
-rwxr-xr-x | src/conf_mode/protocols_pim.py | 207 | ||||
-rwxr-xr-x | src/conf_mode/protocols_pim6.py | 133 | ||||
-rwxr-xr-x | src/migration-scripts/pim/0-to-1 | 72 | ||||
-rwxr-xr-x | src/op_mode/restart_frr.py | 2 |
5 files changed, 307 insertions, 247 deletions
diff --git a/src/conf_mode/protocols_igmp.py b/src/conf_mode/protocols_igmp.py deleted file mode 100755 index 435189025..000000000 --- a/src/conf_mode/protocols_igmp.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2020-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 - -from ipaddress import IPv4Address -from sys import exit - -from vyos import ConfigError -from vyos.config import Config -from vyos.utils.process import process_named_running -from vyos.utils.process import call -from vyos.template import render -from signal import SIGTERM - -from vyos import airbag -airbag.enable() - -# Required to use the full path to pimd, in another case daemon will not be started -pimd_cmd = f'/usr/lib/frr/pimd -d -F traditional --daemon -A 127.0.0.1' - -config_file = r'/tmp/igmp.frr' - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - igmp_conf = { - 'igmp_conf' : False, - 'pim_conf' : False, - 'igmp_proxy_conf' : False, - 'old_ifaces' : {}, - 'ifaces' : {} - } - if not (conf.exists('protocols igmp') or conf.exists_effective('protocols igmp')): - return None - - if conf.exists('protocols igmp-proxy'): - igmp_conf['igmp_proxy_conf'] = True - - if conf.exists('protocols pim'): - igmp_conf['pim_conf'] = True - - if conf.exists('protocols igmp'): - igmp_conf['igmp_conf'] = True - - conf.set_level('protocols igmp') - - # # Get interfaces - for iface in conf.list_effective_nodes('interface'): - igmp_conf['old_ifaces'].update({ - iface : { - 'version' : conf.return_effective_value('interface {0} version'.format(iface)), - 'query_interval' : conf.return_effective_value('interface {0} query-interval'.format(iface)), - 'query_max_resp_time' : conf.return_effective_value('interface {0} query-max-response-time'.format(iface)), - 'gr_join' : {} - } - }) - for gr_join in conf.list_effective_nodes('interface {0} join'.format(iface)): - igmp_conf['old_ifaces'][iface]['gr_join'][gr_join] = conf.return_effective_values('interface {0} join {1} source'.format(iface, gr_join)) - - for iface in conf.list_nodes('interface'): - igmp_conf['ifaces'].update({ - iface : { - 'version' : conf.return_value('interface {0} version'.format(iface)), - 'query_interval' : conf.return_value('interface {0} query-interval'.format(iface)), - 'query_max_resp_time' : conf.return_value('interface {0} query-max-response-time'.format(iface)), - 'gr_join' : {} - } - }) - for gr_join in conf.list_nodes('interface {0} join'.format(iface)): - igmp_conf['ifaces'][iface]['gr_join'][gr_join] = conf.return_values('interface {0} join {1} source'.format(iface, gr_join)) - - return igmp_conf - -def verify(igmp): - if igmp is None: - return None - - if igmp['igmp_conf']: - # Check conflict with IGMP-Proxy - if igmp['igmp_proxy_conf']: - raise ConfigError(f"IGMP proxy and PIM cannot be both configured at the same time") - - # Check interfaces - if not igmp['ifaces']: - raise ConfigError(f"IGMP require defined interfaces!") - # Check, is this multicast group - for intfc in igmp['ifaces']: - for gr_addr in igmp['ifaces'][intfc]['gr_join']: - if not IPv4Address(gr_addr).is_multicast: - raise ConfigError(gr_addr + " not a multicast group") - -def generate(igmp): - if igmp is None: - return None - - render(config_file, 'frr/igmp.frr.j2', igmp) - return None - -def apply(igmp): - if igmp is None: - return None - - pim_pid = process_named_running('pimd') - if igmp['igmp_conf'] or igmp['pim_conf']: - if not pim_pid: - call(pimd_cmd) - - if os.path.exists(config_file): - call(f'vtysh -d pimd -f {config_file}') - os.remove(config_file) - elif pim_pid: - os.kill(int(pim_pid), SIGTERM) - - 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/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py index 0aaa0d2c6..09c3be8df 100755 --- a/src/conf_mode/protocols_pim.py +++ b/src/conf_mode/protocols_pim.py @@ -16,144 +16,139 @@ import os -from ipaddress import IPv4Address +from ipaddress import IPv4Network +from signal import SIGTERM from sys import exit from vyos.config import Config -from vyos import ConfigError +from vyos.config import config_dict_merge +from vyos.configdict import node_changed +from vyos.configverify import verify_interface_exists from vyos.utils.process import process_named_running from vyos.utils.process import call -from vyos.template import render -from signal import SIGTERM - +from vyos.template import render_to_string +from vyos import ConfigError +from vyos import frr from vyos import airbag airbag.enable() -# Required to use the full path to pimd, in another case daemon will not be started -pimd_cmd = f'/usr/lib/frr/pimd -d -F traditional --daemon -A 127.0.0.1' - -config_file = r'/tmp/pimd.frr' - def get_config(config=None): if config: conf = config else: conf = Config() - pim_conf = { - 'pim_conf' : False, - 'igmp_conf' : False, - 'igmp_proxy_conf' : False, - 'old_pim' : { - 'ifaces' : {}, - 'rp' : {} - }, - 'pim' : { - 'ifaces' : {}, - 'rp' : {} - } - } - if not (conf.exists('protocols pim') or conf.exists_effective('protocols pim')): - return None - - if conf.exists('protocols igmp-proxy'): - pim_conf['igmp_proxy_conf'] = True - - if conf.exists('protocols igmp'): - pim_conf['igmp_conf'] = True - - if conf.exists('protocols pim'): - pim_conf['pim_conf'] = True - - conf.set_level('protocols pim') - - # Get interfaces - for iface in conf.list_effective_nodes('interface'): - pim_conf['old_pim']['ifaces'].update({ - iface : { - 'hello' : conf.return_effective_value('interface {0} hello'.format(iface)), - 'dr_prio' : conf.return_effective_value('interface {0} dr-priority'.format(iface)) - } - }) - for iface in conf.list_nodes('interface'): - pim_conf['pim']['ifaces'].update({ - iface : { - 'hello' : conf.return_value('interface {0} hello'.format(iface)), - 'dr_prio' : conf.return_value('interface {0} dr-priority'.format(iface)), - } - }) - - conf.set_level('protocols pim rp') - - # Get RPs addresses - for rp_addr in conf.list_effective_nodes('address'): - pim_conf['old_pim']['rp'][rp_addr] = conf.return_effective_values('address {0} group'.format(rp_addr)) - - for rp_addr in conf.list_nodes('address'): - pim_conf['pim']['rp'][rp_addr] = conf.return_values('address {0} group'.format(rp_addr)) - - # Get RP keep-alive-timer - if conf.exists_effective('rp keep-alive-timer'): - pim_conf['old_pim']['rp_keep_alive'] = conf.return_effective_value('rp keep-alive-timer') - if conf.exists('rp keep-alive-timer'): - pim_conf['pim']['rp_keep_alive'] = conf.return_value('rp keep-alive-timer') - - return pim_conf + base = ['protocols', 'pim'] + + pim = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + + # We can not run both IGMP proxy and PIM at the same time - get IGMP + # proxy status + if conf.exists(['protocols', 'igmp-proxy']): + pim.update({'igmp_proxy_enabled' : {}}) + + # FRR has VRF support for different routing daemons. As interfaces belong + # to VRFs - or the global VRF, we need to check for changed interfaces so + # that they will be properly rendered for the FRR config. Also this eases + # removal of interfaces from the running configuration. + interfaces_removed = node_changed(conf, base + ['interface']) + if interfaces_removed: + pim['interface_removed'] = list(interfaces_removed) + + # Bail out early if configuration tree does no longer exist. this must + # be done after retrieving the list of interfaces to be removed. + if not conf.exists(base): + pim.update({'deleted' : ''}) + return pim + + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = conf.get_config_defaults(**pim.kwargs, recursive=True) + + # We have to cleanup the default dict, as default values could enable features + # which are not explicitly enabled on the CLI. Example: default-information + # originate comes with a default metric-type of 2, which will enable the + # entire default-information originate tree, even when not set via CLI so we + # need to check this first and probably drop that key. + for interface in pim.get('interface', []): + # We need to reload the defaults on every pass b/c of + # hello-multiplier dependency on dead-interval + # If hello-multiplier is set, we need to remove the default from + # dead-interval. + if 'igmp' not in pim['interface'][interface]: + del default_values['interface'][interface]['igmp'] + + pim = config_dict_merge(default_values, pim) + return pim def verify(pim): - if pim is None: + if not pim or 'deleted' in pim: return None - if pim['pim_conf']: - # Check conflict with IGMP-Proxy - if pim['igmp_proxy_conf']: - raise ConfigError(f"IGMP proxy and PIM cannot be both configured at the same time") - - # Check interfaces - if not pim['pim']['ifaces']: - raise ConfigError(f"PIM require defined interfaces!") + if 'igmp_proxy_enabled' in pim: + raise ConfigError('IGMP proxy and PIM cannot be configured at the same time!') - if not pim['pim']['rp']: - raise ConfigError(f"RP address required") + if 'interface' not in pim: + raise ConfigError('PIM require defined interfaces!') - # Check unique multicast groups - uniq_groups = [] - for rp_addr in pim['pim']['rp']: - if not pim['pim']['rp'][rp_addr]: - raise ConfigError(f"Group should be specified for RP " + rp_addr) - for group in pim['pim']['rp'][rp_addr]: - if (group in uniq_groups): - raise ConfigError(f"Group range " + group + " specified cannot exact match another") + for interface in pim['interface']: + verify_interface_exists(interface) - # Check, is this multicast group - gr_addr = group.split('/') - if IPv4Address(gr_addr[0]) < IPv4Address('224.0.0.0'): - raise ConfigError(group + " not a multicast group") + if 'rp' in pim: + if 'address' not in pim['rp']: + raise ConfigError('PIM rendezvous point needs to be defined!') - uniq_groups.extend(pim['pim']['rp'][rp_addr]) + # Check unique multicast groups + unique = [] + pim_base_error = 'PIM rendezvous point group' + for address, address_config in pim['rp']['address'].items(): + if 'group' not in address_config: + raise ConfigError(f'{pim_base_error} should be defined for "{address}"!') + + # Check if it is a multicast group + for gr_addr in address_config['group']: + if not IPv4Network(gr_addr).is_multicast: + raise ConfigError(f'{pim_base_error} "{gr_addr}" is not a multicast group!') + if gr_addr in unique: + raise ConfigError(f'{pim_base_error} must be unique!') + unique.append(gr_addr) def generate(pim): - if pim is None: + if not pim or 'deleted' in pim: return None - - render(config_file, 'frr/pimd.frr.j2', pim) + pim['frr_pimd_config'] = render_to_string('frr/pimd.frr.j2', pim) return None def apply(pim): - if pim is None: + pim_daemon = 'pimd' + pim_pid = process_named_running(pim_daemon) + + if not pim or 'deleted' in pim: + if 'deleted' in pim: + os.kill(int(pim_pid), SIGTERM) + return None - pim_pid = process_named_running('pimd') - if pim['igmp_conf'] or pim['pim_conf']: - if not pim_pid: - call(pimd_cmd) + if not pim_pid: + call('/usr/lib/frr/pimd -d -F traditional --daemon -A 127.0.0.1') + + # Save original configuration prior to starting any commit actions + frr_cfg = frr.FRRConfig() + + frr_cfg.load_configuration(pim_daemon) + frr_cfg.modify_section(f'^ip pim') + frr_cfg.modify_section(f'^ip igmp') - if os.path.exists(config_file): - call("vtysh -d pimd -f " + config_file) - os.remove(config_file) - elif pim_pid: - os.kill(int(pim_pid), SIGTERM) + for key in ['interface', 'interface_removed']: + if key not in pim: + continue + for interface in pim[key]: + frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) + if 'frr_pimd_config' in pim: + frr_cfg.add_before(frr.default_add_before, pim['frr_pimd_config']) + frr_cfg.commit_configuration(pim_daemon) return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_pim6.py b/src/conf_mode/protocols_pim6.py new file mode 100755 index 000000000..2003a1014 --- /dev/null +++ b/src/conf_mode/protocols_pim6.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 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/>. + +from ipaddress import IPv6Address +from ipaddress import IPv6Network +from sys import exit + +from vyos.config import Config +from vyos.config import config_dict_merge +from vyos.configdict import node_changed +from vyos.configverify import verify_interface_exists +from vyos.template import render_to_string +from vyos import ConfigError +from vyos import frr +from vyos import airbag +airbag.enable() + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['protocols', 'pim6'] + pim6 = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, with_recursive_defaults=True) + + # FRR has VRF support for different routing daemons. As interfaces belong + # to VRFs - or the global VRF, we need to check for changed interfaces so + # that they will be properly rendered for the FRR config. Also this eases + # removal of interfaces from the running configuration. + interfaces_removed = node_changed(conf, base + ['interface']) + if interfaces_removed: + pim6['interface_removed'] = list(interfaces_removed) + + # Bail out early if configuration tree does no longer exist. this must + # be done after retrieving the list of interfaces to be removed. + if not conf.exists(base): + pim6.update({'deleted' : ''}) + return pim6 + + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = conf.get_config_defaults(**pim6.kwargs, recursive=True) + + pim6 = config_dict_merge(default_values, pim6) + return pim6 + +def verify(pim6): + if not pim6 or 'deleted' in pim6: + return + + for interface, interface_config in pim6.get('interface', {}).items(): + verify_interface_exists(interface) + if 'mld' in interface_config: + mld = interface_config['mld'] + for group in mld.get('join', {}).keys(): + # Validate multicast group address + if not IPv6Address(group).is_multicast: + raise ConfigError(f"{group} is not a multicast group") + + if 'rp' in pim6: + if 'address' not in pim6['rp']: + raise ConfigError('PIM6 rendezvous point needs to be defined!') + + # Check unique multicast groups + unique = [] + pim_base_error = 'PIM6 rendezvous point group' + + if {'address', 'prefix-list6'} <= set(pim6['rp']): + raise ConfigError(f'{pim_base_error} supports either address or a prefix-list!') + + for address, address_config in pim6['rp']['address'].items(): + if 'group' not in address_config: + raise ConfigError(f'{pim_base_error} should be defined for "{address}"!') + + # Check if it is a multicast group + for gr_addr in address_config['group']: + if not IPv6Network(gr_addr).is_multicast: + raise ConfigError(f'{pim_base_error} "{gr_addr}" is not a multicast group!') + if gr_addr in unique: + raise ConfigError(f'{pim_base_error} must be unique!') + unique.append(gr_addr) + +def generate(pim6): + if not pim6 or 'deleted' in pim6: + return + pim6['new_frr_config'] = render_to_string('frr/pim6d.frr.j2', pim6) + return None + +def apply(pim6): + if pim6 is None: + return + + pim6_daemon = 'pim6d' + + # Save original configuration prior to starting any commit actions + frr_cfg = frr.FRRConfig() + + frr_cfg.load_configuration(pim6_daemon) + + for key in ['interface', 'interface_removed']: + if key not in pim6: + continue + for interface in pim6[key]: + frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) + + if 'new_frr_config' in pim6: + frr_cfg.add_before(frr.default_add_before, pim6['new_frr_config']) + frr_cfg.commit_configuration(pim6_daemon) + 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/migration-scripts/pim/0-to-1 b/src/migration-scripts/pim/0-to-1 new file mode 100755 index 000000000..bf8af733c --- /dev/null +++ b/src/migration-scripts/pim/0-to-1 @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 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/>. + +# T5736: igmp: migrate "protocols igmp" to "protocols pim" + +import sys +from vyos.configtree import ConfigTree + +if len(sys.argv) < 2: + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +base = ['protocols', 'igmp'] +pim_base = ['protocols', 'pim'] +if not config.exists(base): + # Nothing to do + sys.exit(0) + +for interface in config.list_nodes(base + ['interface']): + base_igmp_iface = base + ['interface', interface] + pim_base_iface = pim_base + ['interface', interface] + + # Create IGMP note under PIM interface + if not config.exists(pim_base_iface + ['igmp']): + config.set(pim_base_iface + ['igmp']) + + if config.exists(base_igmp_iface + ['join']): + config.copy(base_igmp_iface + ['join'], pim_base_iface + ['igmp', 'join']) + config.set_tag(pim_base_iface + ['igmp', 'join']) + + new_join_base = pim_base_iface + ['igmp', 'join'] + for address in config.list_nodes(new_join_base): + if config.exists(new_join_base + [address, 'source']): + config.rename(new_join_base + [address, 'source'], 'source-address') + + if config.exists(base_igmp_iface + ['query-interval']): + config.copy(base_igmp_iface + ['query-interval'], pim_base_iface + ['igmp', 'query-interval']) + + if config.exists(base_igmp_iface + ['query-max-response-time']): + config.copy(base_igmp_iface + ['query-max-response-time'], pim_base_iface + ['igmp', 'query-max-response-time']) + + if config.exists(base_igmp_iface + ['version']): + config.copy(base_igmp_iface + ['version'], pim_base_iface + ['igmp', 'version']) + +config.delete(base) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py index 5cce377eb..8841b0eca 100755 --- a/src/op_mode/restart_frr.py +++ b/src/op_mode/restart_frr.py @@ -139,7 +139,7 @@ def _reload_config(daemon): # 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') +cmd_args_parser.add_argument('--daemon', choices=['zebra', 'staticd', 'bgpd', 'eigrpd', 'ospfd', 'ospf6d', 'ripd', 'ripngd', 'isisd', 'pimd', 'pim6d', 'ldpd', 'babeld', 'bfdd'], required=False, nargs='*', help='select single or multiple daemons') # parse arguments cmd_args = cmd_args_parser.parse_args() |