diff options
-rw-r--r-- | data/templates/frr/daemons.frr.tmpl | 2 | ||||
-rw-r--r-- | data/templates/frr/pim6d.frr.j2 | 38 | ||||
-rw-r--r-- | interface-definitions/protocols-pim6.xml.in | 132 | ||||
-rw-r--r-- | python/vyos/frr.py | 2 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_protocols_pim6.py | 124 | ||||
-rwxr-xr-x | src/conf_mode/protocols_pim6.py | 102 |
6 files changed, 399 insertions, 1 deletions
diff --git a/data/templates/frr/daemons.frr.tmpl b/data/templates/frr/daemons.frr.tmpl index 3aad8e8dd..e09c7d1d2 100644 --- a/data/templates/frr/daemons.frr.tmpl +++ b/data/templates/frr/daemons.frr.tmpl @@ -6,6 +6,7 @@ ripd=yes ripngd=yes isisd=yes pimd=no +pim6d=yes ldpd=yes nhrpd=no eigrpd=yes @@ -38,6 +39,7 @@ 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 -%} " diff --git a/data/templates/frr/pim6d.frr.j2 b/data/templates/frr/pim6d.frr.j2 new file mode 100644 index 000000000..8e430541d --- /dev/null +++ b/data/templates/frr/pim6d.frr.j2 @@ -0,0 +1,38 @@ +! +{% if interface is vyos_defined %} +{% for iface, iface_config in interface.items() %} +interface {{ iface }} +{% 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 %} diff --git a/interface-definitions/protocols-pim6.xml.in b/interface-definitions/protocols-pim6.xml.in new file mode 100644 index 000000000..58ef5a1e3 --- /dev/null +++ b/interface-definitions/protocols-pim6.xml.in @@ -0,0 +1,132 @@ +<?xml version="1.0"?> +<!-- Protocol Independent Multicast for IPv6 (PIMv6) configuration --> +<interfaceDefinition> + <node name="protocols"> + <children> + <node name="pim6" owner="${vyos_conf_scripts_dir}/protocols_pim6.py"> + <properties> + <help>Protocol Independent Multicast for IPv6 (PIMv6)</help> + <priority>400</priority> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>PIMv6 interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + </properties> + <children> + <node name="mld"> + <properties> + <help>Multicast Listener Discovery (MLD)</help> + </properties> + <children> + #include <include/generic-disable-node.xml.i> + <tagNode name="join"> + <properties> + <help>MLD join multicast group</help> + <valueHelp> + <format>ipv6</format> + <description>Multicast group address</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + <children> + <leafNode name="source"> + <properties> + <help>Source address</help> + <valueHelp> + <format>ipv6</format> + <description>Source address</description> + </valueHelp> + <completionHelp> + <script>${vyos_completion_dir}/list_local_ips.sh --ipv6</script> + </completionHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="version"> + <properties> + <help>MLD version</help> + <completionHelp> + <list>1 2</list> + </completionHelp> + <valueHelp> + <format>1</format> + <description>MLD version 1</description> + </valueHelp> + <valueHelp> + <format>2</format> + <description>MLD version 2</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-2"/> + </constraint> + </properties> + <defaultValue>2</defaultValue> + </leafNode> + <leafNode name="interval"> + <properties> + <help>Query interval</help> + <valueHelp> + <format>u32:1-65535</format> + <description>Query interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="max-response-time"> + <properties> + <help>Max query response time</help> + <valueHelp> + <format>u32:100-6553500</format> + <description>Query response value in milliseconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 100-6553500"/> + </constraint> + </properties> + </leafNode> + <leafNode name="last-member-query-count"> + <properties> + <help>Last member query count</help> + <valueHelp> + <format>u32:1-255</format> + <description>Count</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + <leafNode name="last-member-query-interval"> + <properties> + <help>Last member query interval</help> + <valueHelp> + <format>u32:100-6553500</format> + <description>Last member query interval in milliseconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 100-6553500"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/frr.py b/python/vyos/frr.py index 2e3c8a271..9c9e50ff7 100644 --- a/python/vyos/frr.py +++ b/python/vyos/frr.py @@ -88,7 +88,7 @@ LOG.addHandler(ch2) _frr_daemons = ['zebra', 'bgpd', 'fabricd', 'isisd', 'ospf6d', 'ospfd', 'pbrd', 'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd', - 'bfdd', 'eigrpd', 'babeld'] + 'bfdd', 'eigrpd', 'babeld' ,'pim6d'] path_vtysh = '/usr/bin/vtysh' path_frr_reload = '/usr/lib/frr/frr-reload.py' diff --git a/smoketest/scripts/cli/test_protocols_pim6.py b/smoketest/scripts/cli/test_protocols_pim6.py new file mode 100755 index 000000000..788329740 --- /dev/null +++ b/smoketest/scripts/cli/test_protocols_pim6.py @@ -0,0 +1,124 @@ +#!/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/>. + +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() + + def test_pim6_01_defaults(self): + # commit changes + self.cli_set(base_path) + self.cli_commit() + + interfaces = Section.interfaces('ethernet') + + # Verify FRR pim6d configuration + for interface in interfaces: + config = self.getFRRconfig( + f'interface {interface}', daemon=PROCESS_NAME) + self.assertIn(f'interface {interface}', config) + self.assertNotIn(f' ipv6 mld', config) + + def test_pim6_02_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_03_mld_join(self): + # commit changes + interfaces = Section.interfaces('ethernet') + + # Use an invalid multiple 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 multiple 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', '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) + + +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..6a1235ba5 --- /dev/null +++ b/src/conf_mode/protocols_pim6.py @@ -0,0 +1,102 @@ +#!/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 sys import exit +from typing import Optional + +from vyos import ConfigError, airbag, frr +from vyos.config import Config, ConfigDict +from vyos.configdict import node_changed +from vyos.configverify import verify_interface_exists +from vyos.template import render_to_string + +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) + + return pim6 + + +def verify(pim6): + if pim6 is None: + 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") + + +def generate(pim6): + if pim6 is None: + return + + pim6['new_frr_config'] = render_to_string('frr/pim6d.frr.j2', pim6) + + +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) + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) |