From 6b44aa0692653493b8d0e0c639a4369860ec603f Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Sun, 12 Nov 2023 18:30:15 +0100 Subject: pim: T5733: add missing FRR PIM related features Migrate CLI configuration retrival to common get_config_dict(). In addition add new functionality to VyOS that is PIM related and already available in FRR. (cherry picked from commit 9abc02edcc237760f1f8aa1b3f08d7f4d18f866c) # Conflicts: # python/vyos/frr.py # src/op_mode/restart_frr.py --- src/conf_mode/protocols_pim.py | 203 ++++++++++++++++++++--------------------- src/op_mode/restart_frr.py | 2 +- 2 files changed, 98 insertions(+), 107 deletions(-) (limited to 'src') diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py index 0aaa0d2c6..89db69b87 100755 --- a/src/conf_mode/protocols_pim.py +++ b/src/conf_mode/protocols_pim.py @@ -16,144 +16,135 @@ 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 dict_merge +from vyos.configdict import node_changed +from vyos.utils.dict import dict_search 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") + if 'igmp_proxy_enabled' in pim: + raise ConfigError('IGMP proxy and PIM cannot be configured at the same time!') - # Check interfaces - if not pim['pim']['ifaces']: - raise ConfigError(f"PIM require defined interfaces!") + if 'interface' not in pim: + raise ConfigError('PIM require defined interfaces!') - if not pim['pim']['rp']: - raise ConfigError(f"RP address required") + if dict_search('rp.address', pim) == None: + raise ConfigError('PIM rendezvous point needs to be defined!') - # 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") + # Check unique multicast groups + unique = [] + for address, address_config in pim['rp']['address'].items(): + if 'group' not in address_config: + raise ConfigError(f'PIM rendezvous point group should be defined for "{address}"!') - # 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") - - uniq_groups.extend(pim['pim']['rp'][rp_addr]) + # 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 rendezvous point group "{gr_addr}" is not a multicast group!') + if gr_addr in unique: + raise ConfigError('PIM rendezvous point group 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/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() -- cgit v1.2.3 From 383231dae8c01ea58d0c956688ecd87f739be97e Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Mon, 13 Nov 2023 12:54:25 +0100 Subject: igmp: T5736: migrate "protocols igmp" to "protocols pim" IGMP and PIM are two different but related things. FRR has both combined in pimd. As we use get_config_dict() and FRR reload it is better to have both centrally stored under the same CLI node (as FRR does, too) to just "fire and forget" the commit to the daemon. "set protocols igmp interface eth1" -> "set protocols pim interface eth1 igmp" (cherry picked from commit bc83fb097719f5c4c803808572f690fbc367b9e5) --- data/configd-include.json | 1 - data/templates/frr/igmp.frr.j2 | 41 ------ data/templates/frr/pimd.frr.j2 | 17 +++ .../include/source-address-ipv4-multi.xml.i | 18 +++ .../include/version/pim-version.xml.i | 3 + interface-definitions/protocols-igmp.xml.in | 95 -------------- interface-definitions/protocols-pim.xml.in | 43 ++++++- interface-definitions/xml-component-version.xml.in | 1 + op-mode-definitions/show-ip-igmp.xml.in | 12 +- smoketest/config-tests/igmp-pim-small | 17 +++ smoketest/configs/igmp-pim-small | 84 +++++++++++++ smoketest/scripts/cli/test_protocols_pim.py | 47 ++++++- src/conf_mode/protocols_igmp.py | 140 --------------------- src/conf_mode/protocols_pim.py | 34 ++--- src/migration-scripts/pim/0-to-1 | 72 +++++++++++ 15 files changed, 322 insertions(+), 303 deletions(-) delete mode 100644 data/templates/frr/igmp.frr.j2 create mode 100644 interface-definitions/include/source-address-ipv4-multi.xml.i create mode 100644 interface-definitions/include/version/pim-version.xml.i delete mode 100644 interface-definitions/protocols-igmp.xml.in create mode 100644 smoketest/config-tests/igmp-pim-small create mode 100644 smoketest/configs/igmp-pim-small delete mode 100755 src/conf_mode/protocols_igmp.py create mode 100755 src/migration-scripts/pim/0-to-1 (limited to 'src') diff --git a/data/configd-include.json b/data/configd-include.json index 84bc1f14e..a762a6d4c 100644 --- a/data/configd-include.json +++ b/data/configd-include.json @@ -44,7 +44,6 @@ "policy-local-route.py", "protocols_bfd.py", "protocols_bgp.py", -"protocols_igmp.py", "protocols_isis.py", "protocols_mpls.py", "protocols_nhrp.py", diff --git a/data/templates/frr/igmp.frr.j2 b/data/templates/frr/igmp.frr.j2 deleted file mode 100644 index b75884484..000000000 --- a/data/templates/frr/igmp.frr.j2 +++ /dev/null @@ -1,41 +0,0 @@ -! -{% for iface in old_ifaces %} -interface {{ iface }} -{% for group in old_ifaces[iface].gr_join %} -{% if old_ifaces[iface].gr_join[group] %} -{% for source in old_ifaces[iface].gr_join[group] %} - no ip igmp join {{ group }} {{ source }} -{% endfor %} -{% else %} - no ip igmp join {{ group }} -{% endif %} -{% endfor %} - no ip igmp -! -{% endfor %} -{% for interface, interface_config in ifaces.items() %} -interface {{ interface }} -{% if interface_config.version %} - ip igmp version {{ interface_config.version }} -{% else %} -{# IGMP default version 3 #} - ip igmp -{% endif %} -{% if interface_config.query_interval %} - ip igmp query-interval {{ interface_config.query_interval }} -{% endif %} -{% if interface_config.query_max_resp_time %} - ip igmp query-max-response-time {{ interface_config.query_max_resp_time }} -{% endif %} -{% for group, sources in interface_config.gr_join.items() %} -{% if sources is vyos_defined %} -{% for source in sources %} - ip igmp join {{ group }} {{ source }} -{% endfor %} -{% else %} - ip igmp join {{ group }} -{% endif %} -{% endfor %} -! -{% endfor %} -! diff --git a/data/templates/frr/pimd.frr.j2 b/data/templates/frr/pimd.frr.j2 index 7d6ddf8d4..97c5ff58b 100644 --- a/data/templates/frr/pimd.frr.j2 +++ b/data/templates/frr/pimd.frr.j2 @@ -27,9 +27,26 @@ interface {{ iface }} {% if iface_config.igmp is vyos_defined %} ip igmp {% endif %} +{% if iface_config.igmp.query_interval %} + ip igmp query-interval {{ iface_config.igmp.query_interval }} +{% endif %} +{% if iface_config.igmp.query_max_response_time %} + ip igmp query-max-response-time {{ iface_config.igmp.query_max_response_time }} +{% endif %} {% if iface_config.igmp.version is vyos_defined %} ip igmp version {{ iface_config.igmp.version }} {% endif %} +{% if iface_config.igmp.join is vyos_defined %} +{% for join, join_config in iface_config.igmp.join.items() %} +{% if join_config.source_address is vyos_defined %} +{% for source_address in join_config.source_address %} + ip igmp join {{ join }} {{ source_address }} +{% endfor %} +{% else %} + ip igmp join {{ join }} +{% endif %} +{% endfor %} +{% endif %} exit {% endfor %} {% endif %} diff --git a/interface-definitions/include/source-address-ipv4-multi.xml.i b/interface-definitions/include/source-address-ipv4-multi.xml.i new file mode 100644 index 000000000..319a118f3 --- /dev/null +++ b/interface-definitions/include/source-address-ipv4-multi.xml.i @@ -0,0 +1,18 @@ + + + + IPv4 source address used to initiate connection + + + + + ipv4 + IPv4 source address + + + + + + + + diff --git a/interface-definitions/include/version/pim-version.xml.i b/interface-definitions/include/version/pim-version.xml.i new file mode 100644 index 000000000..24cc38cdf --- /dev/null +++ b/interface-definitions/include/version/pim-version.xml.i @@ -0,0 +1,3 @@ + + + diff --git a/interface-definitions/protocols-igmp.xml.in b/interface-definitions/protocols-igmp.xml.in deleted file mode 100644 index a055db71e..000000000 --- a/interface-definitions/protocols-igmp.xml.in +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - - Internet Group Management Protocol (IGMP) - - - - - IGMP interface - - - - - - - - IGMP join multicast group - - ipv4 - Multicast group address - - - - - - - - - Source address - - ipv4 - Source address - - - - - - - - - - - - IGMP version - - 2 3 - - - 2 - IGMP version 2 - - - 3 - IGMP version 3 - - - - - - - - - IGMP host query interval - - u32:1-1800 - Query interval in seconds - - - - - - - - - IGMP max query response time - - u32:10-250 - Query response value in deci-seconds - - - - - - - - - - - - - diff --git a/interface-definitions/protocols-pim.xml.in b/interface-definitions/protocols-pim.xml.in index 02a8a6f5e..c1fa1b489 100644 --- a/interface-definitions/protocols-pim.xml.in +++ b/interface-definitions/protocols-pim.xml.in @@ -5,7 +5,7 @@ - Protocol Independent Multicast (PIM) + Protocol Independent Multicast (PIM) and IGMP 400 @@ -31,11 +31,50 @@ Internet Group Management Protocol (IGMP) options + + + IGMP join multicast group + + ipv4 + Multicast group address + + + + + + + #include + + + + + IGMP host query interval + + u32:1-1800 + Query interval in seconds + + + + + + + + + IGMP max query response time + + u32:10-250 + Query response value in deci-seconds + + + + + + Interface IGMP version - 1 2 + 2 3 2 diff --git a/interface-definitions/xml-component-version.xml.in b/interface-definitions/xml-component-version.xml.in index 8c9e816d1..51a28ef57 100644 --- a/interface-definitions/xml-component-version.xml.in +++ b/interface-definitions/xml-component-version.xml.in @@ -29,6 +29,7 @@ #include #include #include + #include #include #include #include diff --git a/op-mode-definitions/show-ip-igmp.xml.in b/op-mode-definitions/show-ip-igmp.xml.in index 855c5d508..1fd86ba54 100644 --- a/op-mode-definitions/show-ip-igmp.xml.in +++ b/op-mode-definitions/show-ip-igmp.xml.in @@ -13,31 +13,31 @@ IGMP groups information - vtysh -c "show ip igmp groups" + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ - + IGMP interfaces information - vtysh -c "show ip igmp interface" + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ IGMP static join information - vtysh -c "show ip igmp join" + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ IGMP sources information - vtysh -c "show ip igmp sources" + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ IGMP statistics - vtysh -c "show ip igmp statistics" + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ diff --git a/smoketest/config-tests/igmp-pim-small b/smoketest/config-tests/igmp-pim-small new file mode 100644 index 000000000..207c17d45 --- /dev/null +++ b/smoketest/config-tests/igmp-pim-small @@ -0,0 +1,17 @@ +set interfaces ethernet eth1 address '100.64.0.1/24' +set interfaces ethernet eth2 address '172.16.0.2/24' +set protocols pim interface eth1 igmp join 224.1.0.0 source-address '1.1.1.1' +set protocols pim interface eth1 igmp join 224.1.0.0 source-address '1.1.1.2' +set protocols pim interface eth1 igmp query-interval '1000' +set protocols pim interface eth1 igmp query-max-response-time '30' +set protocols pim interface eth1 igmp version '2' +set protocols pim interface eth2 +set protocols pim rp address 172.16.255.1 group '224.0.0.0/4' +set service ntp server 0.pool.ntp.org +set service ntp server 1.pool.ntp.org +set service ntp server 2.pool.ntp.org +set system domain-name 'vyos.io' +set system host-name 'vyos' +set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/' +set system login user vyos authentication plaintext-password '' +set system console device ttyS0 speed '115200' diff --git a/smoketest/configs/igmp-pim-small b/smoketest/configs/igmp-pim-small new file mode 100644 index 000000000..f433ff8d7 --- /dev/null +++ b/smoketest/configs/igmp-pim-small @@ -0,0 +1,84 @@ +interfaces { + ethernet eth0 { + duplex auto + speed auto + } + ethernet eth1 { + address 100.64.0.1/24 + duplex auto + speed auto + } + ethernet eth2 { + address 172.16.0.2/24 + duplex auto + speed auto + } +} +protocols { + igmp { + interface eth1 { + join 224.1.0.0 { + source 1.1.1.1 + source 1.1.1.2 + } + query-interval 1000 + query-max-response-time 30 + version 2 + } + } + pim { + interface eth1 { + } + interface eth2 { + } + rp { + address 172.16.255.1 { + group 224.0.0.0/4 + } + } + } +} +system { + config-management { + commit-revisions 200 + } + console { + device ttyS0 { + speed 115200 + } + } + domain-name vyos.io + host-name vyos + login { + user vyos { + authentication { + encrypted-password $6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/ + plaintext-password "" + } + } + } + ntp { + server 0.pool.ntp.org { + } + server 1.pool.ntp.org { + } + server 2.pool.ntp.org { + } + } + syslog { + global { + facility all { + level info + } + facility protocols { + level debug + } + } + } + time-zone Europe/Berlin +} + + +// Warning: Do not remove the following line. +// vyos-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack@1:conntrack-sync@1:dhcp-relay@2:dhcp-server@5:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@18:ipoe-server@1:ipsec@5:l2tp@3:lldp@1:mdns@1:nat@5:ntp@1:pppoe-server@5:pptp@2:qos@1:quagga@7:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@20:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1" +// Release version: 1.3.0 diff --git a/smoketest/scripts/cli/test_protocols_pim.py b/smoketest/scripts/cli/test_protocols_pim.py index 07c806126..ef134b195 100755 --- a/smoketest/scripts/cli/test_protocols_pim.py +++ b/smoketest/scripts/cli/test_protocols_pim.py @@ -77,8 +77,6 @@ class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase): igmp_proxy = ['protocols', 'igmp-proxy'] rp = '127.0.0.1' group = '224.0.0.0/4' - hello = '100' - dr_priority = '64' self.cli_set(base_path) self.cli_set(igmp_proxy) @@ -97,5 +95,50 @@ class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase): # commit changes self.cli_commit() + def test_03_igmp(self): + watermark_warning = '2000' + query_interval = '1000' + query_max_response_time = '200' + version = '2' + + igmp_join = { + '224.1.1.1' : { 'source' : ['1.1.1.1', '2.2.2.2', '3.3.3.3'] }, + '224.1.2.2' : { 'source' : [] }, + '224.1.3.3' : {}, + } + + self.cli_set(base_path + ['igmp', 'watermark-warning', watermark_warning]) + interfaces = Section.interfaces('ethernet') + for interface in interfaces: + self.cli_set(base_path + ['interface', interface , 'igmp', 'version', version]) + self.cli_set(base_path + ['interface', interface , 'igmp', 'query-interval', query_interval]) + self.cli_set(base_path + ['interface', interface , 'igmp', 'query-max-response-time', query_max_response_time]) + + for join, join_config in igmp_join.items(): + self.cli_set(base_path + ['interface', interface , 'igmp', 'join', join]) + if 'source' in join_config: + for source in join_config['source']: + self.cli_set(base_path + ['interface', interface , 'igmp', 'join', join, 'source-address', source]) + + self.cli_commit() + + frrconfig = self.getFRRconfig(daemon=PROCESS_NAME) + self.assertIn(f'ip igmp watermark-warn {watermark_warning}', frrconfig) + + for interface in interfaces: + frrconfig = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + self.assertIn(f'interface {interface}', frrconfig) + self.assertIn(f' ip igmp', frrconfig) + self.assertIn(f' ip igmp version {version}', frrconfig) + self.assertIn(f' ip igmp query-interval {query_interval}', frrconfig) + self.assertIn(f' ip igmp query-max-response-time {query_max_response_time}', frrconfig) + + for join, join_config in igmp_join.items(): + if 'source' in join_config: + for source in join_config['source']: + self.assertIn(f' ip igmp join {join} {source}', frrconfig) + else: + self.assertIn(f' ip igmp join {join}', frrconfig) + if __name__ == '__main__': unittest.main(verbosity=2, failfast=True) 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 . - -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 89db69b87..fbe95c404 100755 --- a/src/conf_mode/protocols_pim.py +++ b/src/conf_mode/protocols_pim.py @@ -93,22 +93,24 @@ def verify(pim): if 'interface' not in pim: raise ConfigError('PIM require defined interfaces!') - if dict_search('rp.address', pim) == None: - raise ConfigError('PIM rendezvous point needs to be defined!') - - # Check unique multicast groups - unique = [] - for address, address_config in pim['rp']['address'].items(): - if 'group' not in address_config: - raise ConfigError(f'PIM rendezvous point group 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 rendezvous point group "{gr_addr}" is not a multicast group!') - if gr_addr in unique: - raise ConfigError('PIM rendezvous point group must be unique!') - unique.append(gr_addr) + if 'rp' in pim: + if 'address' not in pim['rp']: + raise ConfigError('PIM rendezvous point needs to be defined!') + + # 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 not pim or 'deleted' in pim: 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 . + +# 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) -- cgit v1.2.3 From ce21078a983552db3f36ee95ca6bc55dd15fa170 Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Mon, 13 Nov 2023 21:36:31 +0100 Subject: pim: T5733: fix CLI level of global PIM commands (cherry picked from commit dd13213ae94f071bc30cc17f5fabef02fbf95939) --- data/templates/frr/pimd.frr.j2 | 17 +++-- .../include/pim/keep-alive-timer.xml.i | 1 - .../include/policy/prefix-list.xml.i | 14 ++++ interface-definitions/protocols-pim.xml.in | 78 +++++++++------------- op-mode-definitions/show-ip-pim.xml.in | 2 +- smoketest/scripts/cli/test_protocols_pim.py | 54 ++++++++++++++- src/conf_mode/protocols_pim.py | 4 ++ 7 files changed, 110 insertions(+), 60 deletions(-) create mode 100644 interface-definitions/include/policy/prefix-list.xml.i (limited to 'src') diff --git a/data/templates/frr/pimd.frr.j2 b/data/templates/frr/pimd.frr.j2 index 97c5ff58b..b01fb5ec7 100644 --- a/data/templates/frr/pimd.frr.j2 +++ b/data/templates/frr/pimd.frr.j2 @@ -58,13 +58,13 @@ ip pim ecmp {{ 'rebalance' if ecmp.rebalance is vyos_defined }} ip pim join-prune-interval {{ join_prune_interval }} {% endif %} {% if keep_alive_timer is vyos_defined %} -ip pim rp keep-alive-timer {{ keep_alive_timer }} +ip pim keep-alive-timer {{ keep_alive_timer }} {% endif %} {% if packets is vyos_defined %} ip pim packets {{ packets }} {% endif %} -{% if register_accept_list is vyos_defined %} -ip pim register-accept-list {{ register_accept_list }} +{% if register_accept_list.prefix_list is vyos_defined %} +ip pim register-accept-list {{ register_accept_list.prefix_list }} {% endif %} {% if register_suppress_time is vyos_defined %} ip pim register-suppress-time {{ register_suppress_time }} @@ -76,14 +76,17 @@ ip pim rp {{ address }} {{ group }} {% endfor %} {% endfor %} {% endif %} -{% if send_v6_secondary is vyos_defined %} -ip pim send-v6-secondary +{% if rp.keep_alive_timer is vyos_defined %} +ip pim rp keep-alive-timer {{ rp.keep_alive_timer }} +{% endif %} +{% if no_v6_secondary is vyos_defined %} +no ip pim send-v6-secondary {% endif %} {% if spt_switchover.infinity_and_beyond is vyos_defined %} ip pim spt-switchover infinity-and-beyond {{ 'prefix-list ' ~ spt_switchover.infinity_and_beyond.prefix_list if spt_switchover.infinity_and_beyond.prefix_list is defined }} {% endif %} -{% if ssm is vyos_defined %} -ip pim ssm {{ ssm }} +{% if ssm.prefix_list is vyos_defined %} +ip pim ssm prefix-list {{ ssm.prefix_list }} {% endif %} ! {% if igmp.watermark_warning is vyos_defined %} diff --git a/interface-definitions/include/pim/keep-alive-timer.xml.i b/interface-definitions/include/pim/keep-alive-timer.xml.i index 9e71b7a14..0dd27d6e7 100644 --- a/interface-definitions/include/pim/keep-alive-timer.xml.i +++ b/interface-definitions/include/pim/keep-alive-timer.xml.i @@ -10,6 +10,5 @@ - 210 diff --git a/interface-definitions/include/policy/prefix-list.xml.i b/interface-definitions/include/policy/prefix-list.xml.i new file mode 100644 index 000000000..5d7980ee2 --- /dev/null +++ b/interface-definitions/include/policy/prefix-list.xml.i @@ -0,0 +1,14 @@ + + + + Prefix-list to use + + txt + Prefix-list to apply (IPv4) + + + policy prefix-list + + + + diff --git a/interface-definitions/protocols-pim.xml.in b/interface-definitions/protocols-pim.xml.in index c1fa1b489..bbdb00cae 100644 --- a/interface-definitions/protocols-pim.xml.in +++ b/interface-definitions/protocols-pim.xml.in @@ -130,18 +130,14 @@ #include #include #include - + Only accept registers from a specific source prefix list - - txt - Prefix-list to apply - - - policy prefix-list - - + + #include + + Rendezvous Point @@ -174,50 +170,36 @@ - - - Send v6 secondary addresses - - - - + #include + + + + + Disable IPv6 secondary address in hello packets + + + + + + Send v6 secondary addresses + + + - Send v6 secondary addresses + Never switch to SPT Tree - - - Never switch to SPT Tree - - - - - Prefix-List to control which groups to switch - - txt - Prefix-list to apply - - - policy prefix-list - - - - - + #include - - - Source-Specific Multicast - - policy prefix-list - - - txt - Prefix-list to apply - - - + + + + + Source-Specific Multicast + + + #include diff --git a/op-mode-definitions/show-ip-pim.xml.in b/op-mode-definitions/show-ip-pim.xml.in index 3e0bff064..9deba1f07 100644 --- a/op-mode-definitions/show-ip-pim.xml.in +++ b/op-mode-definitions/show-ip-pim.xml.in @@ -63,7 +63,7 @@ ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ - + PIM interfaces information diff --git a/smoketest/scripts/cli/test_protocols_pim.py b/smoketest/scripts/cli/test_protocols_pim.py index ef134b195..ccfced138 100755 --- a/smoketest/scripts/cli/test_protocols_pim.py +++ b/smoketest/scripts/cli/test_protocols_pim.py @@ -73,7 +73,55 @@ class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase): self.cli_commit() - def test_02_pim_igmp_proxy(self): + def test_02_pim_advanced(self): + rp = '127.0.0.2' + group = '224.0.0.0/4' + join_prune_interval = '123' + rp_keep_alive_timer = '190' + keep_alive_timer = '180' + packets = '10' + prefix_list = 'pim-test' + register_suppress_time = '300' + + self.cli_set(base_path + ['rp', 'address', rp, 'group', group]) + self.cli_set(base_path + ['rp', 'keep-alive-timer', rp_keep_alive_timer]) + + self.cli_set(base_path + ['ecmp', 'rebalance']) + self.cli_set(base_path + ['join-prune-interval', join_prune_interval]) + self.cli_set(base_path + ['keep-alive-timer', keep_alive_timer]) + self.cli_set(base_path + ['packets', packets]) + self.cli_set(base_path + ['register-accept-list', 'prefix-list', prefix_list]) + self.cli_set(base_path + ['register-suppress-time', register_suppress_time]) + self.cli_set(base_path + ['no-v6-secondary']) + self.cli_set(base_path + ['spt-switchover', 'infinity-and-beyond', 'prefix-list', prefix_list]) + self.cli_set(base_path + ['ssm', 'prefix-list', prefix_list]) + + # check validate() - PIM require defined interfaces! + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + interfaces = Section.interfaces('ethernet') + for interface in interfaces: + self.cli_set(base_path + ['interface', interface]) + + # commit changes + self.cli_commit() + + # Verify FRR pimd configuration + frrconfig = self.getFRRconfig(daemon=PROCESS_NAME) + self.assertIn(f'ip pim rp {rp} {group}', frrconfig) + self.assertIn(f'ip pim rp keep-alive-timer {rp_keep_alive_timer}', frrconfig) + self.assertIn(f'ip pim ecmp rebalance', frrconfig) + self.assertIn(f'ip pim join-prune-interval {join_prune_interval}', frrconfig) + self.assertIn(f'ip pim keep-alive-timer {keep_alive_timer}', frrconfig) + self.assertIn(f'ip pim packets {packets}', frrconfig) + self.assertIn(f'ip pim register-accept-list {prefix_list}', frrconfig) + self.assertIn(f'ip pim register-suppress-time {register_suppress_time}', frrconfig) + self.assertIn(f'no ip pim send-v6-secondary', frrconfig) + self.assertIn(f'ip pim spt-switchover infinity-and-beyond prefix-list {prefix_list}', frrconfig) + self.assertIn(f'ip pim ssm prefix-list {prefix_list}', frrconfig) + + def test_03_pim_igmp_proxy(self): igmp_proxy = ['protocols', 'igmp-proxy'] rp = '127.0.0.1' group = '224.0.0.0/4' @@ -95,7 +143,7 @@ class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase): # commit changes self.cli_commit() - def test_03_igmp(self): + def test_04_igmp(self): watermark_warning = '2000' query_interval = '1000' query_max_response_time = '200' @@ -141,4 +189,4 @@ class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase): self.assertIn(f' ip igmp join {join}', frrconfig) if __name__ == '__main__': - unittest.main(verbosity=2, failfast=True) + unittest.main(verbosity=2) diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py index fbe95c404..5e6225f6f 100755 --- a/src/conf_mode/protocols_pim.py +++ b/src/conf_mode/protocols_pim.py @@ -25,6 +25,7 @@ from vyos.config import config_dict_merge from vyos.configdict import dict_merge from vyos.configdict import node_changed from vyos.utils.dict import dict_search +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_to_string @@ -93,6 +94,9 @@ def verify(pim): if 'interface' not in pim: raise ConfigError('PIM require defined interfaces!') + for interface in pim['interface']: + verify_interface_exists(interface) + if 'rp' in pim: if 'address' not in pim['rp']: raise ConfigError('PIM rendezvous point needs to be defined!') -- cgit v1.2.3 From 101a0f0f003b30d6023ad79e4a827aa67abf26d7 Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Mon, 13 Nov 2023 22:19:44 +0100 Subject: pim6: T5733: add missing FRR PIM6 related features (cherry picked from commit 403d2ffd6e46cb082b1d16ddf515e1784bee968c) # Conflicts: # data/templates/frr/pim6d.frr.j2 # interface-definitions/protocols-pim6.xml.in # smoketest/scripts/cli/test_protocols_pim6.py # src/conf_mode/protocols_pim6.py --- data/templates/frr/pim6d.frr.j2 | 81 ++++++++++ .../include/policy/prefix-list6.xml.i | 14 ++ interface-definitions/protocols-pim6.xml.in | 179 +++++++++++++++++++++ op-mode-definitions/show-ipv6-mld.xml.in | 42 +++++ op-mode-definitions/show-ipv6-pim.xml.in | 120 ++++++++++++++ smoketest/scripts/cli/test_protocols_pim6.py | 139 ++++++++++++++++ src/conf_mode/protocols_pim6.py | 133 +++++++++++++++ 7 files changed, 708 insertions(+) create mode 100644 data/templates/frr/pim6d.frr.j2 create mode 100644 interface-definitions/include/policy/prefix-list6.xml.i create mode 100644 interface-definitions/protocols-pim6.xml.in create mode 100644 op-mode-definitions/show-ipv6-mld.xml.in create mode 100644 op-mode-definitions/show-ipv6-pim.xml.in create mode 100755 smoketest/scripts/cli/test_protocols_pim6.py create mode 100755 src/conf_mode/protocols_pim6.py (limited to 'src') diff --git a/data/templates/frr/pim6d.frr.j2 b/data/templates/frr/pim6d.frr.j2 new file mode 100644 index 000000000..bac716fcc --- /dev/null +++ b/data/templates/frr/pim6d.frr.j2 @@ -0,0 +1,81 @@ +! +{% if interface is vyos_defined %} +{% for iface, iface_config in interface.items() %} +! +interface {{ iface }} + ipv6 pim +{% if iface_config.no_bsm is vyos_defined %} + no ipv6 pim bsm +{% endif %} +{% if iface_config.dr_priority is vyos_defined %} + ipv6 pim drpriority {{ iface_config.dr_priority }} +{% endif %} +{% if iface_config.hello is vyos_defined %} + ipv6 pim hello {{ iface_config.hello }} +{% endif %} +{% if iface_config.no_unicast_bsm is vyos_defined %} + no ipv6 pim unicast-bsm +{% endif %} +{% if iface_config.passive is vyos_defined %} + ipv6 pim passive +{% endif %} +{% if iface_config.mld is vyos_defined and iface_config.mld.disable is not vyos_defined %} + ipv6 mld +{% if iface_config.mld.version is vyos_defined %} + ipv6 mld version {{ iface_config.mld.version }} +{% endif %} +{% if iface_config.mld.interval is vyos_defined %} + ipv6 mld query-interval {{ iface_config.mld.interval }} +{% endif %} +{% if iface_config.mld.max_response_time is vyos_defined %} + ipv6 mld query-max-response-time {{ iface_config.mld.max_response_time // 100 }} +{% endif %} +{% if iface_config.mld.last_member_query_count is vyos_defined %} + ipv6 mld last-member-query-count {{ iface_config.mld.last_member_query_count }} +{% endif %} +{% if iface_config.mld.last_member_query_interval is vyos_defined %} + ipv6 mld last-member-query-interval {{ iface_config.mld.last_member_query_interval // 100 }} +{% endif %} +{% if iface_config.mld.join is vyos_defined %} +{% for group, group_config in iface_config.mld.join.items() %} +{% if group_config.source is vyos_defined %} +{% for source in group_config.source %} + ipv6 mld join {{ group }} {{ source }} +{% endfor %} +{% else %} + ipv6 mld join {{ group }} +{% endif %} +{% endfor %} +{% endif %} +{% endif %} +exit +{% endfor %} +{% endif %} +! +{% if join_prune_interval is vyos_defined %} +ipv6 pim join-prune-interval {{ join_prune_interval }} +{% endif %} +{% if keep_alive_timer is vyos_defined %} +ipv6 pim keep-alive-timer {{ keep_alive_timer }} +{% endif %} +{% if packets is vyos_defined %} +ipv6 pim packets {{ packets }} +{% endif %} +{% if register_suppress_time is vyos_defined %} +ipv6 pim register-suppress-time {{ register_suppress_time }} +{% endif %} +{% if rp.address is vyos_defined %} +{% for address, address_config in rp.address.items() %} +{% if address_config.group is vyos_defined %} +{% for group in address_config.group %} +ipv6 pim rp {{ address }} {{ group }} +{% endfor %} +{% endif %} +{% if address_config.prefix_list6 is vyos_defined %} +ipv6 pim rp {{ address }} prefix-list {{ address_config.prefix_list6 }} +{% endif %} +{% endfor %} +{% endif %} +{% if rp.keep_alive_timer is vyos_defined %} +ipv6 pim rp keep-alive-timer {{ rp.keep_alive_timer }} +{% endif %} diff --git a/interface-definitions/include/policy/prefix-list6.xml.i b/interface-definitions/include/policy/prefix-list6.xml.i new file mode 100644 index 000000000..101702f1f --- /dev/null +++ b/interface-definitions/include/policy/prefix-list6.xml.i @@ -0,0 +1,14 @@ + + + + Prefix-list to use + + txt + Prefix-list to apply (IPv6) + + + policy prefix-list6 + + + + diff --git a/interface-definitions/protocols-pim6.xml.in b/interface-definitions/protocols-pim6.xml.in new file mode 100644 index 000000000..8bd3f3fee --- /dev/null +++ b/interface-definitions/protocols-pim6.xml.in @@ -0,0 +1,179 @@ + + + + + + + + Protocol Independent Multicast for IPv6 (PIMv6) and MLD + 400 + + + + + PIMv6 interface + + + + + #include + + + + #include + #include + #include + #include + + + Multicast Listener Discovery (MLD) + + + #include + + + MLD join multicast group + + ipv6 + Multicast group address + + + + + + + + + Source address + + ipv6 + Source address + + + + + + + + + + + + + + + Last member query count + + u32:1-255 + Count + + + + + + + + + Last member query interval + + u32:100-6553500 + Last member query interval in milliseconds + + + + + + + + + Query interval + + u32:1-65535 + Query interval in seconds + + + + + + + + + Max query response time + + u32:100-6553500 + Query response value in milliseconds + + + + + + + + + MLD version + + 1 2 + + + 1 + MLD version 1 + + + 2 + MLD version 2 + + + + + + 2 + + + + + + #include + #include + #include + #include + + + Rendezvous Point + + + + + Rendezvous Point address + + ipv6 + Rendezvous Point address + + + + + + + + + Group Address range + + ipv6net + Group Address range + + + + + + + + #include + + + #include + + + + + + + diff --git a/op-mode-definitions/show-ipv6-mld.xml.in b/op-mode-definitions/show-ipv6-mld.xml.in new file mode 100644 index 000000000..5c719f700 --- /dev/null +++ b/op-mode-definitions/show-ipv6-mld.xml.in @@ -0,0 +1,42 @@ + + + + + + + + + Show MLD (Multicast Listener Discovery) information + + + + + MLD group information + + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ + + + + MLD interface information + + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ + + + + MLD joined groups and sources + + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ + + + + MLD statistics + + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ + + + + + + + + diff --git a/op-mode-definitions/show-ipv6-pim.xml.in b/op-mode-definitions/show-ipv6-pim.xml.in new file mode 100644 index 000000000..7cc3ce742 --- /dev/null +++ b/op-mode-definitions/show-ipv6-pim.xml.in @@ -0,0 +1,120 @@ + + + + + + + + + Show PIM (Protocol Independent Multicast) information + + + + + PIM cached bsm packets information + + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ + + + + PIM boot-strap router information + + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ + + + + PIM cached group-rp mappings information + + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ + + + + PIM downstream channel info + + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ + + + + PIM interfaces information + + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ + + + + PIM join information + + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ + + + + PIM interface local-membership + + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ + + + + PIM neighbor information + + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ + + + + PIM cached nexthop rpf information + + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ + + + + PIM rendezvous point information + + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ + + + + PIM reverse path forwarding information + + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ + + + + PIM neighbor addresses + + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ + + + + PIM state information + + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ + + + + PIM statistics + + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ + + + + PIM upstream information + + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ + + + + PIM upstream join-desired + + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ + + + + PIM upstream source reverse path forwarding + + ${vyos_op_scripts_dir}/vtysh_wrapper.sh $@ + + + + + + + + diff --git a/smoketest/scripts/cli/test_protocols_pim6.py b/smoketest/scripts/cli/test_protocols_pim6.py new file mode 100755 index 000000000..e22a7c722 --- /dev/null +++ b/smoketest/scripts/cli/test_protocols_pim6.py @@ -0,0 +1,139 @@ +#!/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 . + +import unittest + +from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.configsession import ConfigSessionError +from vyos.ifconfig import Section +from vyos.utils.process import process_named_running + +PROCESS_NAME = 'pim6d' +base_path = ['protocols', 'pim6'] + +class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): + def tearDown(self): + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + + self.cli_delete(base_path) + self.cli_commit() + + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + + def test_pim6_01_mld_simple(self): + # commit changes + interfaces = Section.interfaces('ethernet') + for interface in interfaces: + self.cli_set(base_path + ['interface', interface]) + + self.cli_commit() + + # Verify FRR pim6d configuration + for interface in interfaces: + config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + self.assertIn(f'interface {interface}', config) + self.assertIn(f' ipv6 mld', config) + self.assertNotIn(f' ipv6 mld version 1', config) + + # Change to MLD version 1 + for interface in interfaces: + self.cli_set(base_path + ['interface', interface, 'mld', 'version', '1']) + + self.cli_commit() + + # Verify FRR pim6d configuration + for interface in interfaces: + config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + self.assertIn(f'interface {interface}', config) + self.assertIn(f' ipv6 mld', config) + self.assertIn(f' ipv6 mld version 1', config) + + def test_pim6_02_mld_join(self): + interfaces = Section.interfaces('ethernet') + # Use an invalid multicast group address + for interface in interfaces: + self.cli_set(base_path + ['interface', interface, 'mld', 'join', 'fd00::1234']) + + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['interface']) + + # Use a valid multicast group address + for interface in interfaces: + self.cli_set(base_path + ['interface', interface, 'mld', 'join', 'ff18::1234']) + + self.cli_commit() + + # Verify FRR pim6d configuration + for interface in interfaces: + config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + self.assertIn(f'interface {interface}', config) + self.assertIn(f' ipv6 mld join ff18::1234', config) + + # Join a source-specific multicast group + for interface in interfaces: + self.cli_set(base_path + ['interface', interface, 'mld', 'join', 'ff38::5678', 'source', '2001:db8::5678']) + + self.cli_commit() + + # Verify FRR pim6d configuration + for interface in interfaces: + config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + self.assertIn(f'interface {interface}', config) + self.assertIn(f' ipv6 mld join ff38::5678 2001:db8::5678', config) + + def test_pim6_03_basic(self): + interfaces = Section.interfaces('ethernet') + join_prune_interval = '123' + keep_alive_timer = '77' + packets = '5' + register_suppress_time = '99' + dr_priority = '100' + hello = '50' + + self.cli_set(base_path + ['join-prune-interval', join_prune_interval]) + self.cli_set(base_path + ['keep-alive-timer', keep_alive_timer]) + self.cli_set(base_path + ['packets', packets]) + self.cli_set(base_path + ['register-suppress-time', register_suppress_time]) + + for interface in interfaces: + self.cli_set(base_path + ['interface', interface, 'dr-priority', dr_priority]) + self.cli_set(base_path + ['interface', interface, 'hello', hello]) + self.cli_set(base_path + ['interface', interface, 'no-bsm']) + self.cli_set(base_path + ['interface', interface, 'no-unicast-bsm']) + self.cli_set(base_path + ['interface', interface, 'passive']) + + self.cli_commit() + + # Verify FRR pim6d configuration + config = self.getFRRconfig(daemon=PROCESS_NAME) + self.assertIn(f'ipv6 pim join-prune-interval {join_prune_interval}', config) + self.assertIn(f'ipv6 pim keep-alive-timer {keep_alive_timer}', config) + self.assertIn(f'ipv6 pim packets {packets}', config) + self.assertIn(f'ipv6 pim register-suppress-time {register_suppress_time}', config) + + for interface in interfaces: + config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + self.assertIn(f' ipv6 pim drpriority {dr_priority}', config) + self.assertIn(f' ipv6 pim hello {hello}', config) + self.assertIn(f' no ipv6 pim bsm', config) + self.assertIn(f' no ipv6 pim unicast-bsm', config) + self.assertIn(f' ipv6 pim passive', config) + +if __name__ == '__main__': + unittest.main(verbosity=2) 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 . + +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) -- cgit v1.2.3 From b9493ce110fb1ae2090299a5bcb5d3c7fc3dfd60 Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Wed, 15 Nov 2023 18:20:39 +0100 Subject: pim: T5733: incorporate feedback from peer review (cherry picked from commit 64b4cfc71d402222fd6b034336b3588b5986ba24) --- interface-definitions/include/pim/dr-priority.xml.i | 4 ++-- interface-definitions/include/pim/join-prune-interval.xml.i | 2 +- interface-definitions/include/pim/register-suppress-time.xml.i | 4 ++-- interface-definitions/protocols-pim.xml.in | 2 +- src/conf_mode/protocols_pim.py | 2 -- 5 files changed, 6 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/interface-definitions/include/pim/dr-priority.xml.i b/interface-definitions/include/pim/dr-priority.xml.i index bfda4e257..e4b3067c2 100644 --- a/interface-definitions/include/pim/dr-priority.xml.i +++ b/interface-definitions/include/pim/dr-priority.xml.i @@ -1,10 +1,10 @@ - Designated Router Election Priority + Designated router election priority u32:1-4294967295 - Value of the new DR Priority + DR Priority diff --git a/interface-definitions/include/pim/join-prune-interval.xml.i b/interface-definitions/include/pim/join-prune-interval.xml.i index f7519ad67..882787d3f 100644 --- a/interface-definitions/include/pim/join-prune-interval.xml.i +++ b/interface-definitions/include/pim/join-prune-interval.xml.i @@ -1,7 +1,7 @@ - Join Prune Send Interval + Join prune send interval u32:1-65535 Interval in seconds diff --git a/interface-definitions/include/pim/register-suppress-time.xml.i b/interface-definitions/include/pim/register-suppress-time.xml.i index 80180e9b6..919945b52 100644 --- a/interface-definitions/include/pim/register-suppress-time.xml.i +++ b/interface-definitions/include/pim/register-suppress-time.xml.i @@ -1,10 +1,10 @@ - Register Suppress Timer + Register suppress timer u32:1-65535 - In seconds + Timer in seconds diff --git a/interface-definitions/protocols-pim.xml.in b/interface-definitions/protocols-pim.xml.in index 39188398b..4a20c0d9b 100644 --- a/interface-definitions/protocols-pim.xml.in +++ b/interface-definitions/protocols-pim.xml.in @@ -182,7 +182,7 @@ - Send v6 secondary addresses + Shortest-path tree (SPT) switchover diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py index 5e6225f6f..09c3be8df 100755 --- a/src/conf_mode/protocols_pim.py +++ b/src/conf_mode/protocols_pim.py @@ -22,9 +22,7 @@ from sys import exit from vyos.config import Config from vyos.config import config_dict_merge -from vyos.configdict import dict_merge from vyos.configdict import node_changed -from vyos.utils.dict import dict_search from vyos.configverify import verify_interface_exists from vyos.utils.process import process_named_running from vyos.utils.process import call -- cgit v1.2.3