diff options
Diffstat (limited to 'smoketest/scripts/cli')
62 files changed, 5586 insertions, 1372 deletions
diff --git a/smoketest/scripts/cli/base_accel_ppp_test.py b/smoketest/scripts/cli/base_accel_ppp_test.py index c6f6cb804..750702e98 100644 --- a/smoketest/scripts/cli/base_accel_ppp_test.py +++ b/smoketest/scripts/cli/base_accel_ppp_test.py @@ -14,6 +14,7 @@ import re +from time import sleep from base_vyostest_shim import VyOSUnitTestSHIM from configparser import ConfigParser @@ -641,6 +642,11 @@ delegate={delegate_2_prefix},{delegate_mask},name={pool_name}""" for log_level in range(0, 5): self.set(['log', 'level', str(log_level)]) self.cli_commit() + + # Systemd comes with a default of 5 restarts in 10 seconds policy, + # this limit can be hit by this reastart sequence, slow down a bit + sleep(5) + # Validate configuration values conf = ConfigParser(allow_no_value=True) conf.read(self._config_file) diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index 593b4b415..5348b0cc3 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -1,4 +1,4 @@ -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright (C) 2019-2025 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 @@ -14,11 +14,14 @@ import re +from json import loads from netifaces import AF_INET from netifaces import AF_INET6 from netifaces import ifaddresses +from systemd import journal from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.configsession import ConfigSessionError from vyos.defaults import directories @@ -37,12 +40,15 @@ from vyos.utils.network import is_intf_addr_assigned from vyos.utils.network import is_ipv6_link_local from vyos.utils.network import get_nft_vrf_zone_mapping from vyos.xml_ref import cli_defined +from vyos.xml_ref import default_value dhclient_base_dir = directories['isc_dhclient_dir'] dhclient_process_name = 'dhclient' dhcp6c_base_dir = directories['dhcp6_client_dir'] dhcp6c_process_name = 'dhcp6c' +MSG_TESTCASE_UNSUPPORTED = 'unsupported on interface family' + server_ca_root_cert_data = """ MIIBcTCCARagAwIBAgIUDcAf1oIQV+6WRaW7NPcSnECQ/lUwCgYIKoZIzj0EAwIw HjEcMBoGA1UEAwwTVnlPUyBzZXJ2ZXIgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjBa @@ -133,6 +139,7 @@ def is_mirrored_to(interface, mirror_if, qdisc): if mirror_if in tmp: ret_val = True return ret_val + class BasicInterfaceTest: class TestCase(VyOSUnitTestSHIM.TestCase): _test_dhcp = False @@ -181,6 +188,9 @@ class BasicInterfaceTest: section = Section.section(span) cls.cli_set(cls, ['interfaces', section, span]) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME + @classmethod def tearDownClass(cls): # Tear down mirror interfaces for SPAN (Switch Port Analyzer) @@ -213,7 +223,7 @@ class BasicInterfaceTest: def test_dhcp_disable_interface(self): if not self._test_dhcp: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) # When interface is configured as admin down, it must be admin down # even when dhcpc starts on the given interface @@ -236,7 +246,7 @@ class BasicInterfaceTest: def test_dhcp_client_options(self): if not self._test_dhcp or not self._test_vrf: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) client_id = 'VyOS-router' distance = '100' @@ -276,7 +286,10 @@ class BasicInterfaceTest: def test_dhcp_vrf(self): if not self._test_dhcp or not self._test_vrf: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) + + cli_default_metric = default_value(self._base_path + [self._interfaces[0], + 'dhcp-options', 'default-route-distance']) vrf_name = 'purple4' self.cli_set(['vrf', 'name', vrf_name, 'table', '65000']) @@ -303,13 +316,34 @@ class BasicInterfaceTest: self.assertIn(str(dhclient_pid), vrf_pids) # and the commandline has the appropriate options cmdline = read_file(f'/proc/{dhclient_pid}/cmdline') - self.assertIn('-e\x00IF_METRIC=210', cmdline) # 210 is the default value + self.assertIn(f'-e\x00IF_METRIC={cli_default_metric}', cmdline) + + # T5103: remove interface from VRF instance and move DHCP client + # back to default VRF. This must restart the DHCP client process + for interface in self._interfaces: + self.cli_delete(self._base_path + [interface, 'vrf']) + + self.cli_commit() + + # Validate interface state + for interface in self._interfaces: + tmp = get_interface_vrf(interface) + self.assertEqual(tmp, 'default') + # Check if dhclient process runs + dhclient_pid = process_named_running(dhclient_process_name, cmdline=interface, timeout=10) + self.assertTrue(dhclient_pid) + # .. inside the appropriate VRF instance + vrf_pids = cmd(f'ip vrf pids {vrf_name}') + self.assertNotIn(str(dhclient_pid), vrf_pids) + # and the commandline has the appropriate options + cmdline = read_file(f'/proc/{dhclient_pid}/cmdline') + self.assertIn(f'-e\x00IF_METRIC={cli_default_metric}', cmdline) self.cli_delete(['vrf', 'name', vrf_name]) def test_dhcpv6_vrf(self): if not self._test_ipv6_dhcpc6 or not self._test_vrf: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) vrf_name = 'purple6' self.cli_set(['vrf', 'name', vrf_name, 'table', '65001']) @@ -337,11 +371,31 @@ class BasicInterfaceTest: vrf_pids = cmd(f'ip vrf pids {vrf_name}') self.assertIn(str(tmp), vrf_pids) + # T7135: remove interface from VRF instance and move DHCP client + # back to default VRF. This must restart the DHCP client process + for interface in self._interfaces: + self.cli_delete(self._base_path + [interface, 'vrf']) + + self.cli_commit() + + # Validate interface state + for interface in self._interfaces: + tmp = get_interface_vrf(interface) + self.assertEqual(tmp, 'default') + + # Check if dhclient process runs + tmp = process_named_running(dhcp6c_process_name, cmdline=interface, timeout=10) + self.assertTrue(tmp) + # .. inside the appropriate VRF instance + vrf_pids = cmd(f'ip vrf pids {vrf_name}') + self.assertNotIn(str(tmp), vrf_pids) + + self.cli_delete(['vrf', 'name', vrf_name]) def test_move_interface_between_vrf_instances(self): if not self._test_vrf: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) vrf1_name = 'smoketest_mgmt1' vrf1_table = '5424' @@ -386,7 +440,7 @@ class BasicInterfaceTest: def test_add_to_invalid_vrf(self): if not self._test_vrf: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) # move interface into first VRF for interface in self._interfaces: @@ -404,7 +458,7 @@ class BasicInterfaceTest: def test_span_mirror(self): if not self._mirror_interfaces: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) # Check the two-way mirror rules of ingress and egress for mirror in self._mirror_interfaces: @@ -513,7 +567,7 @@ class BasicInterfaceTest: def test_ipv6_link_local_address(self): # Common function for IPv6 link-local address assignemnts if not self._test_ipv6: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) for interface in self._interfaces: base = self._base_path + [interface] @@ -544,7 +598,7 @@ class BasicInterfaceTest: def test_interface_mtu(self): if not self._test_mtu: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) for intf in self._interfaces: base = self._base_path + [intf] @@ -563,8 +617,8 @@ class BasicInterfaceTest: def test_mtu_1200_no_ipv6_interface(self): # Testcase if MTU can be changed to 1200 on non IPv6 # enabled interfaces - if not self._test_mtu: - self.skipTest('not supported') + if not self._test_mtu or not self._test_ipv6: + self.skipTest(MSG_TESTCASE_UNSUPPORTED) old_mtu = self._mtu self._mtu = '1200' @@ -600,7 +654,7 @@ class BasicInterfaceTest: # which creates a wlan0 and wlan1 interface which will fail the # tearDown() test in the end that no interface is allowed to survive! if not self._test_vlan: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) for interface in self._interfaces: base = self._base_path + [interface] @@ -645,7 +699,7 @@ class BasicInterfaceTest: # which creates a wlan0 and wlan1 interface which will fail the # tearDown() test in the end that no interface is allowed to survive! if not self._test_vlan or not self._test_mtu: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) mtu_1500 = '1500' mtu_9000 = '9000' @@ -691,7 +745,7 @@ class BasicInterfaceTest: # which creates a wlan0 and wlan1 interface which will fail the # tearDown() test in the end that no interface is allowed to survive! if not self._test_vlan: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) for interface in self._interfaces: base = self._base_path + [interface] @@ -761,7 +815,7 @@ class BasicInterfaceTest: def test_vif_8021q_lower_up_down(self): # Testcase for https://vyos.dev/T3349 if not self._test_vlan: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) for interface in self._interfaces: base = self._base_path + [interface] @@ -801,7 +855,7 @@ class BasicInterfaceTest: # which creates a wlan0 and wlan1 interface which will fail the # tearDown() test in the end that no interface is allowed to survive! if not self._test_qinq: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) for interface in self._interfaces: base = self._base_path + [interface] @@ -868,7 +922,7 @@ class BasicInterfaceTest: # which creates a wlan0 and wlan1 interface which will fail the # tearDown() test in the end that no interface is allowed to survive! if not self._test_qinq: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) for interface in self._interfaces: base = self._base_path + [interface] @@ -906,7 +960,7 @@ class BasicInterfaceTest: def test_interface_ip_options(self): if not self._test_ip: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) arp_tmo = '300' mss = '1420' @@ -1008,12 +1062,13 @@ class BasicInterfaceTest: def test_interface_ipv6_options(self): if not self._test_ipv6: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) mss = '1400' dad_transmits = '10' accept_dad = '0' source_validation = 'strict' + interface_identifier = '::fffe' for interface in self._interfaces: path = self._base_path + [interface] @@ -1036,6 +1091,9 @@ class BasicInterfaceTest: if cli_defined(self._base_path + ['ipv6'], 'source-validation'): self.cli_set(path + ['ipv6', 'source-validation', source_validation]) + if cli_defined(self._base_path + ['ipv6', 'address'], 'interface-identifier'): + self.cli_set(path + ['ipv6', 'address', 'interface-identifier', interface_identifier]) + self.cli_commit() for interface in self._interfaces: @@ -1067,13 +1125,20 @@ class BasicInterfaceTest: self.assertIn('fib saddr . iif oif 0', line) self.assertIn('drop', line) + if cli_defined(self._base_path + ['ipv6', 'address'], 'interface-identifier'): + tmp = cmd(f'ip -j token show dev {interface}') + tmp = loads(tmp)[0] + self.assertEqual(tmp['token'], interface_identifier) + self.assertEqual(tmp['ifname'], interface) + + def test_dhcpv6_client_options(self): if not self._test_ipv6_dhcpc6: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) duid_base = 10 for interface in self._interfaces: - duid = '00:01:00:01:27:71:db:f0:00:50:00:00:00:{}'.format(duid_base) + duid = f'00:01:00:01:27:71:db:f0:00:50:00:00:00:{duid_base}' path = self._base_path + [interface] for option in self._options.get(interface, []): self.cli_set(path + option.split()) @@ -1090,7 +1155,7 @@ class BasicInterfaceTest: duid_base = 10 for interface in self._interfaces: - duid = '00:01:00:01:27:71:db:f0:00:50:00:00:00:{}'.format(duid_base) + duid = f'00:01:00:01:27:71:db:f0:00:50:00:00:00:{duid_base}' dhcpc6_config = read_file(f'{dhcp6c_base_dir}/dhcp6c.{interface}.conf') self.assertIn(f'interface {interface} ' + '{', dhcpc6_config) self.assertIn(f' request domain-name-servers;', dhcpc6_config) @@ -1102,16 +1167,25 @@ class BasicInterfaceTest: self.assertIn('};', dhcpc6_config) duid_base += 1 + # T7058: verify daemon has no problems understanding the custom DUID option + j = journal.Reader() + j.this_boot() + j.add_match(_SYSTEMD_UNIT=f'dhcp6c@{interface}.service') + for entry in j: + self.assertNotIn('yyerror0', entry.get('MESSAGE', '')) + self.assertNotIn('syntax error', entry.get('MESSAGE', '')) + # Better ask the process about it's commandline in the future pid = process_named_running(dhcp6c_process_name, cmdline=interface, timeout=10) self.assertTrue(pid) + # DHCPv6 option "no-release" requires "-n" daemon startup option dhcp6c_options = read_file(f'/proc/{pid}/cmdline') self.assertIn('-n', dhcp6c_options) def test_dhcpv6pd_auto_sla_id(self): if not self._test_ipv6_pd: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) prefix_len = '56' sla_len = str(64 - int(prefix_len)) @@ -1172,7 +1246,7 @@ class BasicInterfaceTest: def test_dhcpv6pd_manual_sla_id(self): if not self._test_ipv6_pd: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) prefix_len = '56' sla_len = str(64 - int(prefix_len)) @@ -1238,7 +1312,7 @@ class BasicInterfaceTest: def test_eapol(self): if not self._test_eapol: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) cfg_dir = '/run/wpa_supplicant' diff --git a/smoketest/scripts/cli/base_vyostest_shim.py b/smoketest/scripts/cli/base_vyostest_shim.py index 940306ac3..f0674f187 100644 --- a/smoketest/scripts/cli/base_vyostest_shim.py +++ b/smoketest/scripts/cli/base_vyostest_shim.py @@ -29,6 +29,14 @@ from vyos.utils.process import run save_config = '/tmp/vyos-smoketest-save' +# The commit process is not finished until all pending files from +# VYATTA_CHANGES_ONLY_DIR are copied to VYATTA_ACTIVE_CONFIGURATION_DIR. This +# is done inside libvyatta-cfg1 and the FUSE UnionFS part. On large non- +# interactive commits FUSE UnionFS might not replicate the real state in time, +# leading to errors when querying the working and effective configuration. +# TO BE DELETED AFTER SWITCH TO IN MEMORY CONFIG +CSTORE_GUARD_TIME = 4 + # This class acts as shim between individual Smoketests developed for VyOS and # the Python UnitTest framework. Before every test is loaded, we dump the current # system configuration and reload it after the test - despite the test results. @@ -43,7 +51,9 @@ class VyOSUnitTestSHIM: # trigger the certain failure condition. # Use "self.debug = True" in derived classes setUp() method debug = False - + # Time to wait after a commit to ensure the CStore is up to date + # only required for testcases using FRR + _commit_guard_time = 0 @classmethod def setUpClass(cls): cls._session = ConfigSession(os.getpid()) @@ -65,10 +75,11 @@ class VyOSUnitTestSHIM: cls._session.discard() cls.fail(cls) - def cli_set(self, config): + def cli_set(self, path, value=None): if self.debug: - print('set ' + ' '.join(config)) - self._session.set(config) + str = f'set {" ".join(path)} {value}' if value else f'set {" ".join(path)}' + print(str) + self._session.set(path, value) def cli_delete(self, config): if self.debug: @@ -83,10 +94,17 @@ class VyOSUnitTestSHIM: def cli_commit(self): if self.debug: print('commit') - self._session.commit() - # during a commit there is a process opening commit_lock, and run() returns 0 + # During a commit there is a process opening commit_lock, and run() + # returns 0 while run(f'sudo lsof -nP {commit_lock}') == 0: sleep(0.250) + # Return the output of commit + # Necessary for testing Warning cases + out = self._session.commit() + # Wait for CStore completion for fast non-interactive commits + sleep(self._commit_guard_time) + + return out def op_mode(self, path : list) -> None: """ @@ -101,14 +119,36 @@ class VyOSUnitTestSHIM: pprint.pprint(out) return out - def getFRRconfig(self, string=None, end='$', endsection='^!', daemon=''): - """ Retrieve current "running configuration" from FRR """ - command = f'vtysh -c "show run {daemon} no-header"' - if string: command += f' | sed -n "/^{string}{end}/,/{endsection}/p"' + def getFRRconfig(self, string=None, end='$', endsection='^!', + substring=None, endsubsection=None, empty_retry=0): + """ + Retrieve current "running configuration" from FRR + + string: search for a specific start string in the configuration + end: end of the section to search for (line ending) + endsection: end of the configuration + substring: search section under the result found by string + endsubsection: end of the subsection (usually something with "exit") + """ + command = f'vtysh -c "show run no-header"' + if string: + command += f' | sed -n "/^{string}{end}/,/{endsection}/p"' + if substring and endsubsection: + command += f' | sed -n "/^{substring}/,/{endsubsection}/p"' out = cmd(command) if self.debug: print(f'\n\ncommand "{command}" returned:\n') pprint.pprint(out) + if empty_retry > 0: + retry_count = 0 + while not out and retry_count < empty_retry: + if self.debug and retry_count % 10 == 0: + print(f"Attempt {retry_count}: FRR config is still empty. Retrying...") + retry_count += 1 + sleep(1) + out = cmd(command) + if not out: + print(f'FRR configuration still empty after {empty_retry} retires!') return out @staticmethod @@ -147,6 +187,27 @@ class VyOSUnitTestSHIM: break self.assertTrue(not matched if inverse else matched, msg=search) + def verify_nftables_chain_exists(self, table, chain, inverse=False): + try: + cmd(f'sudo nft list chain {table} {chain}') + if inverse: + self.fail(f'Chain exists: {table} {chain}') + except OSError: + if not inverse: + self.fail(f'Chain does not exist: {table} {chain}') + + # Verify ip rule output + def verify_rules(self, rules_search, inverse=False, addr_family='inet'): + rule_output = cmd(f'ip -family {addr_family} rule show') + + for search in rules_search: + matched = False + for line in rule_output.split("\n"): + if all(item in line for item in search): + matched = True + break + self.assertTrue(not matched if inverse else matched, msg=search) + # standard construction; typing suggestion: https://stackoverflow.com/a/70292317 def ignore_warning(warning: Type[Warning]): import warnings diff --git a/smoketest/scripts/cli/test_container.py b/smoketest/scripts/cli/test_container.py index c03b9eb44..daad3a909 100755 --- a/smoketest/scripts/cli/test_container.py +++ b/smoketest/scripts/cli/test_container.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright (C) 2021-2025 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 @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import os import unittest import glob import json @@ -26,27 +27,28 @@ from vyos.utils.process import cmd from vyos.utils.process import process_named_running base_path = ['container'] -cont_image = 'busybox:stable' # busybox is included in vyos-build PROCESS_NAME = 'conmon' PROCESS_PIDFILE = '/run/vyos-container-{0}.service.pid' +busybox_image = 'busybox:stable' busybox_image_path = '/usr/share/vyos/busybox-stable.tar' + def cmd_to_json(command): c = cmd(command + ' --format=json') data = json.loads(c)[0] return data + class TestContainer(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestContainer, cls).setUpClass() - # Load image for smoketest provided in vyos-build - try: - cmd(f'cat {busybox_image_path} | sudo podman load') - except: - cls.skipTest(cls, reason='busybox image not available') + # Load image for smoketest provided in vyos-1x-smoketest + if not os.path.exists(busybox_image_path): + cls.fail(cls, f'{busybox_image} image not available') + cmd(f'sudo podman load -i {busybox_image_path}') # ensure we can also run this test on a live system - so lets clean # out the current configuration :) @@ -55,9 +57,8 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): @classmethod def tearDownClass(cls): super(TestContainer, cls).tearDownClass() - # Cleanup podman image - cmd(f'sudo podman image rm -f {cont_image}') + cmd(f'sudo podman image rm -f {busybox_image}') def tearDown(self): self.cli_delete(base_path) @@ -74,13 +75,26 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): cont_name = 'c1' self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', '10.0.2.15/24']) - self.cli_set(['protocols', 'static', 'route', '0.0.0.0/0', 'next-hop', '10.0.2.2']) + self.cli_set( + ['protocols', 'static', 'route', '0.0.0.0/0', 'next-hop', '10.0.2.2'] + ) self.cli_set(['system', 'name-server', '1.1.1.1']) self.cli_set(['system', 'name-server', '8.8.8.8']) - self.cli_set(base_path + ['name', cont_name, 'image', cont_image]) + self.cli_set(base_path + ['name', cont_name, 'image', busybox_image]) self.cli_set(base_path + ['name', cont_name, 'allow-host-networks']) - self.cli_set(base_path + ['name', cont_name, 'sysctl', 'parameter', 'kernel.msgmax', 'value', '4096']) + self.cli_set( + base_path + + [ + 'name', + cont_name, + 'sysctl', + 'parameter', + 'kernel.msgmax', + 'value', + '4096', + ] + ) # commit changes self.cli_commit() @@ -96,11 +110,54 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): tmp = cmd(f'sudo podman exec -it {cont_name} sysctl kernel.msgmax') self.assertEqual(tmp, 'kernel.msgmax = 4096') + def test_log_driver(self): + self.cli_set(base_path + ['log-driver', 'journald']) + + self.cli_commit() + + tmp = cmd('podman info --format "{{ .Host.LogDriver }}"') + self.assertEqual(tmp, 'journald') + + def test_name_server(self): + cont_name = 'dns-test' + net_name = 'net-test' + name_server = '192.168.0.1' + prefix = '192.0.2.0/24' + + self.cli_set(base_path + ['network', net_name, 'prefix', prefix]) + + self.cli_set(base_path + ['name', cont_name, 'image', busybox_image]) + self.cli_set(base_path + ['name', cont_name, 'name-server', name_server]) + self.cli_set( + base_path + + [ + 'name', + cont_name, + 'network', + net_name, + 'address', + str(ip_interface(prefix).ip + 2), + ] + ) + + # verify() - name server has no effect when container network has dns enabled + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(base_path + ['network', net_name, 'no-name-server']) + self.cli_commit() + + n = cmd_to_json(f'sudo podman inspect {cont_name}') + self.assertEqual(n['HostConfig']['Dns'][0], name_server) + + tmp = cmd(f'sudo podman exec -it {cont_name} cat /etc/resolv.conf') + self.assertIn(name_server, tmp) + def test_cpu_limit(self): cont_name = 'c2' self.cli_set(base_path + ['name', cont_name, 'allow-host-networks']) - self.cli_set(base_path + ['name', cont_name, 'image', cont_image]) + self.cli_set(base_path + ['name', cont_name, 'image', busybox_image]) self.cli_set(base_path + ['name', cont_name, 'cpu-quota', '1.25']) self.cli_commit() @@ -121,8 +178,18 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): for ii in range(1, 6): name = f'{base_name}-{ii}' - self.cli_set(base_path + ['name', name, 'image', cont_image]) - self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + ii)]) + self.cli_set(base_path + ['name', name, 'image', busybox_image]) + self.cli_set( + base_path + + [ + 'name', + name, + 'network', + net_name, + 'address', + str(ip_interface(prefix).ip + ii), + ] + ) # verify() - first IP address of a prefix can not be used by a container with self.assertRaises(ConfigSessionError): @@ -139,8 +206,14 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): for ii in range(2, 6): name = f'{base_name}-{ii}' c = cmd_to_json(f'sudo podman container inspect {name}') - self.assertEqual(c['NetworkSettings']['Networks'][net_name]['Gateway'] , str(ip_interface(prefix).ip + 1)) - self.assertEqual(c['NetworkSettings']['Networks'][net_name]['IPAddress'], str(ip_interface(prefix).ip + ii)) + self.assertEqual( + c['NetworkSettings']['Networks'][net_name]['Gateway'], + str(ip_interface(prefix).ip + 1), + ) + self.assertEqual( + c['NetworkSettings']['Networks'][net_name]['IPAddress'], + str(ip_interface(prefix).ip + ii), + ) def test_ipv6_network(self): prefix = '2001:db8::/64' @@ -151,8 +224,18 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): for ii in range(1, 6): name = f'{base_name}-{ii}' - self.cli_set(base_path + ['name', name, 'image', cont_image]) - self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + ii)]) + self.cli_set(base_path + ['name', name, 'image', busybox_image]) + self.cli_set( + base_path + + [ + 'name', + name, + 'network', + net_name, + 'address', + str(ip_interface(prefix).ip + ii), + ] + ) # verify() - first IP address of a prefix can not be used by a container with self.assertRaises(ConfigSessionError): @@ -169,8 +252,14 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): for ii in range(2, 6): name = f'{base_name}-{ii}' c = cmd_to_json(f'sudo podman container inspect {name}') - self.assertEqual(c['NetworkSettings']['Networks'][net_name]['IPv6Gateway'] , str(ip_interface(prefix).ip + 1)) - self.assertEqual(c['NetworkSettings']['Networks'][net_name]['GlobalIPv6Address'], str(ip_interface(prefix).ip + ii)) + self.assertEqual( + c['NetworkSettings']['Networks'][net_name]['IPv6Gateway'], + str(ip_interface(prefix).ip + 1), + ) + self.assertEqual( + c['NetworkSettings']['Networks'][net_name]['GlobalIPv6Address'], + str(ip_interface(prefix).ip + ii), + ) def test_dual_stack_network(self): prefix4 = '192.0.2.0/24' @@ -183,9 +272,29 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): for ii in range(1, 6): name = f'{base_name}-{ii}' - self.cli_set(base_path + ['name', name, 'image', cont_image]) - self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix4).ip + ii)]) - self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix6).ip + ii)]) + self.cli_set(base_path + ['name', name, 'image', busybox_image]) + self.cli_set( + base_path + + [ + 'name', + name, + 'network', + net_name, + 'address', + str(ip_interface(prefix4).ip + ii), + ] + ) + self.cli_set( + base_path + + [ + 'name', + name, + 'network', + net_name, + 'address', + str(ip_interface(prefix6).ip + ii), + ] + ) # verify() - first IP address of a prefix can not be used by a container with self.assertRaises(ConfigSessionError): @@ -203,10 +312,22 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): for ii in range(2, 6): name = f'{base_name}-{ii}' c = cmd_to_json(f'sudo podman container inspect {name}') - self.assertEqual(c['NetworkSettings']['Networks'][net_name]['IPv6Gateway'] , str(ip_interface(prefix6).ip + 1)) - self.assertEqual(c['NetworkSettings']['Networks'][net_name]['GlobalIPv6Address'], str(ip_interface(prefix6).ip + ii)) - self.assertEqual(c['NetworkSettings']['Networks'][net_name]['Gateway'] , str(ip_interface(prefix4).ip + 1)) - self.assertEqual(c['NetworkSettings']['Networks'][net_name]['IPAddress'] , str(ip_interface(prefix4).ip + ii)) + self.assertEqual( + c['NetworkSettings']['Networks'][net_name]['IPv6Gateway'], + str(ip_interface(prefix6).ip + 1), + ) + self.assertEqual( + c['NetworkSettings']['Networks'][net_name]['GlobalIPv6Address'], + str(ip_interface(prefix6).ip + ii), + ) + self.assertEqual( + c['NetworkSettings']['Networks'][net_name]['Gateway'], + str(ip_interface(prefix4).ip + 1), + ) + self.assertEqual( + c['NetworkSettings']['Networks'][net_name]['IPAddress'], + str(ip_interface(prefix4).ip + ii), + ) def test_no_name_server(self): prefix = '192.0.2.0/24' @@ -217,20 +338,56 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['network', net_name, 'no-name-server']) name = f'{base_name}-2' - self.cli_set(base_path + ['name', name, 'image', cont_image]) - self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + 2)]) + self.cli_set(base_path + ['name', name, 'image', busybox_image]) + self.cli_set( + base_path + + [ + 'name', + name, + 'network', + net_name, + 'address', + str(ip_interface(prefix).ip + 2), + ] + ) self.cli_commit() n = cmd_to_json(f'sudo podman network inspect {net_name}') self.assertEqual(n['dns_enabled'], False) + def test_network_mtu(self): + prefix = '192.0.2.0/24' + base_name = 'ipv4' + net_name = 'NET01' + + self.cli_set(base_path + ['network', net_name, 'prefix', prefix]) + self.cli_set(base_path + ['network', net_name, 'mtu', '1280']) + + name = f'{base_name}-2' + self.cli_set(base_path + ['name', name, 'image', busybox_image]) + self.cli_set( + base_path + + [ + 'name', + name, + 'network', + net_name, + 'address', + str(ip_interface(prefix).ip + 2), + ] + ) + self.cli_commit() + + n = cmd_to_json(f'sudo podman network inspect {net_name}') + self.assertEqual(n['options']['mtu'], '1280') + def test_uid_gid(self): cont_name = 'uid-test' gid = '100' uid = '1001' self.cli_set(base_path + ['name', cont_name, 'allow-host-networks']) - self.cli_set(base_path + ['name', cont_name, 'image', cont_image]) + self.cli_set(base_path + ['name', cont_name, 'image', busybox_image]) self.cli_set(base_path + ['name', cont_name, 'gid', gid]) # verify() - GID can only be set if UID is set @@ -252,17 +409,20 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): for ii in container_list: name = f'{base_name}-{ii}' - self.cli_set(base_path + ['name', name, 'image', cont_image]) + self.cli_set(base_path + ['name', name, 'image', busybox_image]) self.cli_set(base_path + ['name', name, 'allow-host-networks']) self.cli_commit() # Query API about running containers - tmp = cmd("sudo curl --unix-socket /run/podman/podman.sock -H 'content-type: application/json' -sf http://localhost/containers/json") + tmp = cmd( + "sudo curl --unix-socket /run/podman/podman.sock -H 'content-type: application/json' -sf http://localhost/containers/json" + ) tmp = json.loads(tmp) # We expect the same amount of containers from the API that we started above self.assertEqual(len(container_list), len(tmp)) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index 3e9ec2935..851a15f16 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2024 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 @@ -119,6 +119,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.com']) self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.org']) self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'eth0']) + self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'pod-smoketest']) self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'vtun0']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'accept']) @@ -133,6 +134,9 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'outbound-interface', 'group', '!smoketest_interface']) + # Create container network so test won't fail + self.cli_set(['container', 'network', 'smoketest', 'prefix', '10.0.0.0/24']) + self.cli_commit() self.wait_for_domain_resolver('ip vyos_filter', 'D_smoketest_domain', '192.0.2.5') @@ -248,6 +252,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'action', 'notrack']) self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'protocol', 'tcp']) self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'destination', 'port', '23']) + self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'set', 'mark', '55']) self.cli_commit() @@ -275,7 +280,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['OUT-raw default-action drop', 'drop'], ['chain VYOS_PREROUTING_raw'], ['type filter hook prerouting priority raw; policy accept;'], - ['tcp dport 23', 'notrack'], + ['tcp dport 23', 'meta mark set 0x00000037', 'notrack'], ['PRE-raw default-action accept', 'accept'], ['chain NAME_smoketest'], ['saddr 172.16.20.10', 'daddr 172.16.10.10', 'log prefix "[ipv4-NAM-smoketest-1-A]" log level debug', 'ip ttl 15', 'accept'], @@ -315,6 +320,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'mark', '1010']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'jump']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'jump-target', name]) + self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'set', 'dscp', '32']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '2', 'protocol', 'tcp']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '2', 'mark', '!98765']) @@ -331,7 +337,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): nftables_search = [ ['chain VYOS_FORWARD_filter'], ['type filter hook forward priority filter; policy accept;'], - ['ip saddr 198.51.100.1-198.51.100.50', 'meta mark 0x000003f2', f'jump NAME_{name}'], + ['ip saddr 198.51.100.1-198.51.100.50', 'meta mark 0x000003f2', 'ip dscp set cs4', f'jump NAME_{name}'], ['FWD-filter default-action drop', 'drop'], ['chain VYOS_INPUT_filter'], ['type filter hook input priority filter; policy accept;'], @@ -485,6 +491,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv6', 'prerouting', 'raw', 'rule', '1', 'action', 'drop']) self.cli_set(['firewall', 'ipv6', 'prerouting', 'raw', 'rule', '1', 'protocol', 'tcp']) self.cli_set(['firewall', 'ipv6', 'prerouting', 'raw', 'rule', '1', 'destination', 'port', '23']) + self.cli_set(['firewall', 'ipv6', 'prerouting', 'raw', 'rule', '1', 'set', 'hop-limit', '79']) self.cli_commit() @@ -507,7 +514,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['OUT-raw default-action drop', 'drop'], ['chain VYOS_IPV6_PREROUTING_raw'], ['type filter hook prerouting priority raw; policy accept;'], - ['tcp dport 23', 'drop'], + ['tcp dport 23', 'ip6 hoplimit set 79', 'drop'], ['PRE-raw default-action accept', 'accept'], [f'chain NAME6_{name}'], ['saddr 2002::1-2002::10', 'daddr 2002::1:1', 'log prefix "[ipv6-NAM-v6-smoketest-1-A]" log level crit', 'accept'], @@ -635,6 +642,10 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip6 vyos_filter') def test_ipv4_global_state(self): + self.cli_set(['firewall', 'flowtable', 'smoketest', 'interface', 'eth0']) + self.cli_set(['firewall', 'flowtable', 'smoketest', 'offload', 'software']) + + self.cli_set(['firewall', 'global-options', 'state-policy', 'offload', 'offload-target', 'smoketest']) self.cli_set(['firewall', 'global-options', 'state-policy', 'established', 'action', 'accept']) self.cli_set(['firewall', 'global-options', 'state-policy', 'related', 'action', 'accept']) self.cli_set(['firewall', 'global-options', 'state-policy', 'invalid', 'action', 'drop']) @@ -644,6 +655,9 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): nftables_search = [ ['jump VYOS_STATE_POLICY'], ['chain VYOS_STATE_POLICY'], + ['jump VYOS_STATE_POLICY_FORWARD'], + ['chain VYOS_STATE_POLICY_FORWARD'], + ['flow add @VYOS_FLOWTABLE_smoketest'], ['ct state established', 'accept'], ['ct state invalid', 'drop'], ['ct state related', 'accept'] @@ -651,6 +665,13 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip vyos_filter') + # T7148 - Ensure bridge rule reject -> drop + self.cli_set(['firewall', 'global-options', 'state-policy', 'invalid', 'action', 'reject']) + self.cli_commit() + + self.verify_nftables([['ct state invalid', 'reject']], 'ip vyos_filter') + self.verify_nftables([['ct state invalid', 'drop']], 'bridge vyos_filter') + # Check conntrack is enabled from state-policy self.verify_nftables_chain([['accept']], 'ip vyos_conntrack', 'FW_CONNTRACK') self.verify_nftables_chain([['accept']], 'ip6 vyos_conntrack', 'FW_CONNTRACK') @@ -722,9 +743,12 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '1', 'vlan', 'id', vlan_id]) self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '1', 'vlan', 'ethernet-type', 'ipv4']) + self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '1', 'set', 'connection-mark', '123123']) + self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'action', 'jump']) self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'jump-target', name]) self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'vlan', 'priority', vlan_prior]) + self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'set', 'ttl', '128']) self.cli_set(['firewall', 'bridge', 'input', 'filter', 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'bridge', 'input', 'filter', 'rule', '1', 'inbound-interface', 'name', interface_in]) @@ -746,8 +770,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['chain VYOS_FORWARD_filter'], ['type filter hook forward priority filter; policy accept;'], ['jump VYOS_STATE_POLICY'], - [f'vlan id {vlan_id}', 'vlan type ip', 'accept'], - [f'vlan pcp {vlan_prior}', f'jump NAME_{name}'], + [f'vlan id {vlan_id}', 'vlan type ip', 'ct mark set 0x0001e0f3', 'accept'], + [f'vlan pcp {vlan_prior}', 'ip ttl set 128', f'jump NAME_{name}'], ['log prefix "[bri-FWD-filter-default-D]"', 'drop', 'FWD-filter default-action drop'], [f'chain NAME_{name}'], [f'ether saddr {mac_address}', f'iifname "{interface_in}"', f'log prefix "[bri-NAM-{name}-1-A]" log level crit', 'accept'], @@ -759,6 +783,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['type filter hook output priority filter; policy accept;'], ['ct state invalid', 'udp sport 67', 'udp dport 68', 'accept'], ['ct state invalid', 'ether type arp', 'accept'], + ['ct state invalid', 'ether type 0x8864', 'accept'], ['chain VYOS_PREROUTING_filter'], ['type filter hook prerouting priority filter; policy accept;'], ['ip6 daddr @A6_AGV6', 'notrack'], @@ -899,7 +924,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): def test_zone_basic(self): self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'default-action', 'drop']) self.cli_set(['firewall', 'ipv6', 'name', 'smoketestv6', 'default-action', 'drop']) - self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'interface', 'eth0']) + self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'member', 'interface', 'eth0']) self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'from', 'smoketest-local', 'firewall', 'name', 'smoketest']) self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'intra-zone-filtering', 'firewall', 'ipv6-name', 'smoketestv6']) self.cli_set(['firewall', 'zone', 'smoketest-local', 'local-zone']) @@ -957,6 +982,98 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip vyos_filter') self.verify_nftables(nftables_search_v6, 'ip6 vyos_filter') + def test_zone_with_vrf(self): + self.cli_set(['firewall', 'ipv4', 'name', 'ZONE1-to-LOCAL', 'default-action', 'accept']) + self.cli_set(['firewall', 'ipv4', 'name', 'ZONE2_to_ZONE1', 'default-action', 'continue']) + self.cli_set(['firewall', 'ipv6', 'name', 'LOCAL_to_ZONE2_v6', 'default-action', 'drop']) + self.cli_set(['firewall', 'zone', 'LOCAL', 'from', 'ZONE1', 'firewall', 'name', 'ZONE1-to-LOCAL']) + self.cli_set(['firewall', 'zone', 'LOCAL', 'local-zone']) + self.cli_set(['firewall', 'zone', 'ZONE1', 'from', 'ZONE2', 'firewall', 'name', 'ZONE2_to_ZONE1']) + self.cli_set(['firewall', 'zone', 'ZONE1', 'member', 'interface', 'eth1']) + self.cli_set(['firewall', 'zone', 'ZONE1', 'member', 'interface', 'eth2']) + self.cli_set(['firewall', 'zone', 'ZONE1', 'member', 'vrf', 'VRF-1']) + self.cli_set(['firewall', 'zone', 'ZONE2', 'from', 'LOCAL', 'firewall', 'ipv6-name', 'LOCAL_to_ZONE2_v6']) + self.cli_set(['firewall', 'zone', 'ZONE2', 'member', 'interface', 'vtun66']) + self.cli_set(['firewall', 'zone', 'ZONE2', 'member', 'vrf', 'VRF-2']) + + self.cli_set(['vrf', 'name', 'VRF-1', 'table', '101']) + self.cli_set(['vrf', 'name', 'VRF-2', 'table', '102']) + self.cli_set(['interfaces', 'ethernet', 'eth0', 'vrf', 'VRF-1']) + self.cli_set(['interfaces', 'vti', 'vti1', 'vrf', 'VRF-2']) + + self.cli_commit() + + nftables_search = [ + ['chain NAME_ZONE1-to-LOCAL'], + ['counter', 'accept', 'comment "NAM-ZONE1-to-LOCAL default-action accept"'], + ['chain NAME_ZONE2_to_ZONE1'], + ['counter', 'continue', 'comment "NAM-ZONE2_to_ZONE1 default-action continue"'], + ['chain VYOS_ZONE_FORWARD'], + ['type filter hook forward priority filter + 1'], + ['oifname { "eth1", "eth2" }', 'counter packets', 'jump VZONE_ZONE1'], + ['oifname "eth0"', 'counter packets', 'jump VZONE_ZONE1'], + ['oifname "vtun66"', 'counter packets', 'jump VZONE_ZONE2'], + ['oifname "vti1"', 'counter packets', 'jump VZONE_ZONE2'], + ['chain VYOS_ZONE_LOCAL'], + ['type filter hook input priority filter + 1'], + ['counter packets', 'jump VZONE_LOCAL_IN'], + ['chain VYOS_ZONE_OUTPUT'], + ['type filter hook output priority filter + 1'], + ['counter packets', 'jump VZONE_LOCAL_OUT'], + ['chain VZONE_LOCAL_IN'], + ['iifname { "eth1", "eth2" }', 'counter packets', 'jump NAME_ZONE1-to-LOCAL'], + ['iifname "VRF-1"', 'counter packets', 'jump NAME_ZONE1-to-LOCAL'], + ['counter packets', 'drop', 'comment "zone_LOCAL default-action drop"'], + ['chain VZONE_LOCAL_OUT'], + ['counter packets', 'drop', 'comment "zone_LOCAL default-action drop"'], + ['chain VZONE_ZONE1'], + ['iifname { "eth1", "eth2" }', 'counter packets', 'return'], + ['iifname "VRF-1"', 'counter packets', 'return'], + ['iifname "vtun66"', 'counter packets', 'jump NAME_ZONE2_to_ZONE1'], + ['iifname "vtun66"', 'counter packets', 'return'], + ['iifname "VRF-2"', 'counter packets', 'jump NAME_ZONE2_to_ZONE1'], + ['iifname "VRF-2"', 'counter packets', 'return'], + ['counter packets', 'drop', 'comment "zone_ZONE1 default-action drop"'], + ['chain VZONE_ZONE2'], + ['iifname "vtun66"', 'counter packets', 'return'], + ['iifname "VRF-2"', 'counter packets', 'return'], + ['counter packets', 'drop', 'comment "zone_ZONE2 default-action drop"'] + ] + + nftables_search_v6 = [ + ['chain NAME6_LOCAL_to_ZONE2_v6'], + ['counter', 'drop', 'comment "NAM-LOCAL_to_ZONE2_v6 default-action drop"'], + ['chain VYOS_ZONE_FORWARD'], + ['type filter hook forward priority filter + 1'], + ['oifname { "eth1", "eth2" }', 'counter packets', 'jump VZONE_ZONE1'], + ['oifname "eth0"', 'counter packets', 'jump VZONE_ZONE1'], + ['oifname "vtun66"', 'counter packets', 'jump VZONE_ZONE2'], + ['oifname "vti1"', 'counter packets', 'jump VZONE_ZONE2'], + ['chain VYOS_ZONE_LOCAL'], + ['type filter hook input priority filter + 1'], + ['counter packets', 'jump VZONE_LOCAL_IN'], + ['chain VYOS_ZONE_OUTPUT'], + ['type filter hook output priority filter + 1'], + ['counter packets', 'jump VZONE_LOCAL_OUT'], + ['chain VZONE_LOCAL_IN'], + ['counter packets', 'drop', 'comment "zone_LOCAL default-action drop"'], + ['chain VZONE_LOCAL_OUT'], + ['oifname "vtun66"', 'counter packets', 'jump NAME6_LOCAL_to_ZONE2_v6'], + ['oifname "vti1"', 'counter packets', 'jump NAME6_LOCAL_to_ZONE2_v6'], + ['counter packets', 'drop', 'comment "zone_LOCAL default-action drop"'], + ['chain VZONE_ZONE1'], + ['iifname { "eth1", "eth2" }', 'counter packets', 'return'], + ['iifname "VRF-1"', 'counter packets', 'return'], + ['counter packets', 'drop', 'comment "zone_ZONE1 default-action drop"'], + ['chain VZONE_ZONE2'], + ['iifname "vtun66"', 'counter packets', 'return'], + ['iifname "VRF-2"', 'counter packets', 'return'], + ['counter packets', 'drop', 'comment "zone_ZONE2 default-action drop"'] + ] + + self.verify_nftables(nftables_search, 'ip vyos_filter') + self.verify_nftables(nftables_search_v6, 'ip6 vyos_filter') + def test_flow_offload(self): self.cli_set(['interfaces', 'ethernet', 'eth0', 'vif', '10']) self.cli_set(['firewall', 'flowtable', 'smoketest', 'interface', 'eth0.10']) @@ -1068,7 +1185,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4']) self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'action', 'jump']) self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4']) - + self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'action', 'jump']) self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-out4']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'jump']) @@ -1103,8 +1220,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'action', 'jump']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'jump-target', 'smoketest-cycle-1']) - # nft will fail to load cyclic jumps in any form, whether the rule is reachable or not. - # It should be caught by conf validation. + # nft will fail to load cyclic jumps in any form, whether the rule is reachable or not. + # It should be caught by conf validation. with self.assertRaises(ConfigSessionError): self.cli_commit() @@ -1163,5 +1280,113 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): with self.assertRaises(ConfigSessionError): self.cli_commit() + def test_ipv4_remote_group(self): + # Setup base config for test + self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'url', 'http://127.0.0.1:80/list.txt']) + self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'description', 'Example Group 01']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'action', 'drop']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'protocol', 'tcp']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'destination', 'group', 'remote-group', 'group01']) + + self.cli_commit() + + # Test remote-group had been loaded correctly in nft + nftables_search = [ + ['R_group01'], + ['type ipv4_addr'], + ['flags interval'], + ['meta l4proto', 'daddr @R_group01', 'ipv4-INP-filter-10'] + ] + self.verify_nftables(nftables_search, 'ip vyos_filter') + + # Test remote-group cannot be configured without a URL + self.cli_delete(['firewall', 'group', 'remote-group', 'group01', 'url']) + + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_discard() + + # Test remote-group cannot be set alongside address in rules + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'destination', 'address', '127.0.0.1']) + + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_discard() + + + def test_ipv6_remote_group(self): + # Setup base config for test + self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'url', 'http://127.0.0.1:80/list.txt']) + self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'description', 'Example Group 01']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'action', 'drop']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'protocol', 'tcp']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'destination', 'group', 'remote-group', 'group01']) + + self.cli_commit() + + # Test remote-group had been loaded correctly in nft + nftables_search = [ + ['R6_group01'], + ['type ipv6_addr'], + ['flags interval'], + ['meta l4proto', 'daddr @R6_group01', 'ipv6-INP-filter-10'] + ] + self.verify_nftables(nftables_search, 'ip6 vyos_filter') + + # Test remote-group cannot be configured without a URL + self.cli_delete(['firewall', 'group', 'remote-group', 'group01', 'url']) + + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_discard() + + # Test remote-group cannot be set alongside address in rules + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'destination', 'address', '2001:db8::1']) + + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_discard() + + + def test_remote_group(self): + # Setup base config for test adding remote group to both ipv4 and ipv6 rules + self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'url', 'http://127.0.0.1:80/list.txt']) + self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'description', 'Example Group 01']) + self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '10', 'action', 'drop']) + self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '10', 'protocol', 'tcp']) + self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '10', 'destination', 'group', 'remote-group', 'group01']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'action', 'drop']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'protocol', 'tcp']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'source', 'group', 'remote-group', 'group01']) + self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '10', 'action', 'drop']) + self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '10', 'protocol', 'tcp']) + self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '10', 'destination', 'group', 'remote-group', 'group01']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'action', 'drop']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'protocol', 'tcp']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'source', 'group', 'remote-group', 'group01']) + + self.cli_commit() + + # Test remote-group had been loaded correctly in nft ip table + nftables_v4_search = [ + ['R_group01'], + ['type ipv4_addr'], + ['flags interval'], + ['meta l4proto', 'daddr @R_group01', 'ipv4-OUT-filter-10'], + ['meta l4proto', 'saddr @R_group01', 'ipv4-INP-filter-10'], + ] + self.verify_nftables(nftables_v4_search, 'ip vyos_filter') + + # Test remote-group had been loaded correctly in nft ip6 table + nftables_v6_search = [ + ['R6_group01'], + ['type ipv6_addr'], + ['flags interval'], + ['meta l4proto', 'daddr @R6_group01', 'ipv6-OUT-filter-10'], + ['meta l4proto', 'saddr @R6_group01', 'ipv6-INP-filter-10'], + ] + self.verify_nftables(nftables_v6_search, 'ip6 vyos_filter') + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py index f436424b8..f99fd0363 100755 --- a/smoketest/scripts/cli/test_interfaces_bonding.py +++ b/smoketest/scripts/cli/test_interfaces_bonding.py @@ -167,18 +167,25 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase): def test_bonding_multi_use_member(self): # Define available bonding hash policies - for interface in ['bond10', 'bond20']: + bonds = ['bond10', 'bond20', 'bond30'] + for interface in bonds: for member in self._members: self.cli_set(self._base_path + [interface, 'member', 'interface', member]) # check validate() - can not use the same member interfaces multiple times with self.assertRaises(ConfigSessionError): self.cli_commit() - - self.cli_delete(self._base_path + ['bond20']) + # only keep the first bond interface configuration + for interface in bonds[1:]: + self.cli_delete(self._base_path + [interface]) self.cli_commit() + bond = bonds[0] + member_ifaces = read_file(f'/sys/class/net/{bond}/bonding/slaves').split() + for member in self._members: + self.assertIn(member, member_ifaces) + def test_bonding_source_interface(self): # Re-use member interface that is already a source-interface bond = 'bond99' @@ -286,7 +293,7 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase): id = '5' for interface in self._interfaces: - frrconfig = self.getFRRconfig(f'interface {interface}', daemon='zebra') + frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f' evpn mh es-id {id}', frrconfig) self.assertIn(f' evpn mh es-df-pref {id}', frrconfig) @@ -303,7 +310,7 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase): id = '5' for interface in self._interfaces: - frrconfig = self.getFRRconfig(f'interface {interface}', daemon='zebra') + frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f' evpn mh es-sys-mac 00:12:34:56:78:0{id}', frrconfig) self.assertIn(f' evpn mh uplink', frrconfig) diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py index 124c1fbcb..4041b3ef3 100755 --- a/smoketest/scripts/cli/test_interfaces_bridge.py +++ b/smoketest/scripts/cli/test_interfaces_bridge.py @@ -22,6 +22,7 @@ from base_interfaces_test import BasicInterfaceTest from copy import deepcopy from glob import glob +from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section from vyos.template import ip_from_cidr from vyos.utils.process import cmd @@ -157,6 +158,21 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): # verify member is assigned to the bridge self.assertEqual(interface, tmp['master']) + def test_bridge_multi_use_member(self): + # Define available bonding hash policies + bridges = ['br10', 'br20', 'br30'] + for interface in bridges: + for member in self._members: + self.cli_set(self._base_path + [interface, 'member', 'interface', member]) + + # check validate() - can not use the same member interfaces multiple times + with self.assertRaises(ConfigSessionError): + self.cli_commit() + # only keep the first bond interface configuration + for interface in bridges[1:]: + self.cli_delete(self._base_path + [interface]) + + self.cli_commit() def test_add_remove_bridge_member(self): # Add member interfaces to bridge and set STP cost/priority @@ -460,5 +476,38 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): tmp = get_interface_config(interface) self.assertEqual(protocol, tmp['linkinfo']['info_data']['vlan_protocol']) + def test_bridge_delete_with_vxlan_heighbor_suppress(self): + vxlan_if = 'vxlan0' + vni = '123' + br_if = 'br0' + eth0_addr = '192.0.2.2/30' + + self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', eth0_addr]) + self.cli_set(['interfaces', 'vxlan', vxlan_if, 'parameters', 'neighbor-suppress']) + self.cli_set(['interfaces', 'vxlan', vxlan_if, 'mtu', '1426']) + self.cli_set(['interfaces', 'vxlan', vxlan_if, 'source-address', ip_from_cidr(eth0_addr)]) + self.cli_set(['interfaces', 'vxlan', vxlan_if, 'vni', vni]) + + self.cli_set(['interfaces', 'bridge', br_if, 'member', 'interface', vxlan_if]) + + self.cli_commit() + + self.assertTrue(interface_exists(vxlan_if)) + self.assertTrue(interface_exists(br_if)) + + # cannot delete bridge interface if "neighbor-suppress" parameter is configured for VXLAN interface + self.cli_delete(['interfaces', 'bridge', br_if]) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(['interfaces', 'vxlan', vxlan_if, 'parameters', 'neighbor-suppress']) + + self.cli_commit() + + self.assertFalse(interface_exists(br_if)) + + self.cli_delete(['interfaces', 'vxlan', vxlan_if]) + self.cli_delete(['interfaces', 'ethernet', 'eth0', 'address', eth0_addr]) + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py index 3d12364f7..2b421e942 100755 --- a/smoketest/scripts/cli/test_interfaces_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_ethernet.py @@ -27,10 +27,11 @@ from netifaces import ifaddresses from base_interfaces_test import BasicInterfaceTest from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section -from vyos.utils.process import cmd -from vyos.utils.process import popen from vyos.utils.file import read_file +from vyos.utils.network import is_intf_addr_assigned from vyos.utils.network import is_ipv6_link_local +from vyos.utils.process import cmd +from vyos.utils.process import popen class EthernetInterfaceTest(BasicInterfaceTest.TestCase): @classmethod @@ -77,14 +78,18 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): continue self.assertFalse(is_intf_addr_assigned(interface, addr['addr'])) # Ensure no VLAN interfaces are left behind - tmp = [x for x in Section.interfaces('ethernet') if x.startswith(f'{interface}.')] + tmp = [ + x + for x in Section.interfaces('ethernet') + if x.startswith(f'{interface}.') + ] self.assertListEqual(tmp, []) def test_offloading_rps(self): # enable RPS on all available CPUs, RPS works with a CPU bitmask, # where each bit represents a CPU (core/thread). The formula below # expands to rps_cpus = 255 for a 8 core system - rps_cpus = (1 << os.cpu_count()) -1 + rps_cpus = (1 << os.cpu_count()) - 1 # XXX: we should probably reserve one core when the system is under # high preasure so we can still have a core left for housekeeping. @@ -100,7 +105,7 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): for interface in self._interfaces: cpus = read_file(f'/sys/class/net/{interface}/queues/rx-0/rps_cpus') # remove the nasty ',' separation on larger strings - cpus = cpus.replace(',','') + cpus = cpus.replace(',', '') cpus = int(cpus, 16) self.assertEqual(f'{cpus:x}', f'{rps_cpus:x}') @@ -116,12 +121,14 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): for interface in self._interfaces: queues = len(glob(f'/sys/class/net/{interface}/queues/rx-*')) - rfs_flow = int(global_rfs_flow/queues) + rfs_flow = int(global_rfs_flow / queues) for i in range(0, queues): - tmp = read_file(f'/sys/class/net/{interface}/queues/rx-{i}/rps_flow_cnt') + tmp = read_file( + f'/sys/class/net/{interface}/queues/rx-{i}/rps_flow_cnt' + ) self.assertEqual(int(tmp), rfs_flow) - tmp = read_file(f'/proc/sys/net/core/rps_sock_flow_entries') + tmp = read_file('/proc/sys/net/core/rps_sock_flow_entries') self.assertEqual(int(tmp), global_rfs_flow) # delete configuration of RFS and check all values returned to default "0" @@ -132,12 +139,13 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): for interface in self._interfaces: queues = len(glob(f'/sys/class/net/{interface}/queues/rx-*')) - rfs_flow = int(global_rfs_flow/queues) + rfs_flow = int(global_rfs_flow / queues) for i in range(0, queues): - tmp = read_file(f'/sys/class/net/{interface}/queues/rx-{i}/rps_flow_cnt') + tmp = read_file( + f'/sys/class/net/{interface}/queues/rx-{i}/rps_flow_cnt' + ) self.assertEqual(int(tmp), 0) - def test_non_existing_interface(self): unknonw_interface = self._base_path + ['eth667'] self.cli_set(unknonw_interface) @@ -212,15 +220,24 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): out = loads(out) self.assertFalse(out[0]['autonegotiate']) - def test_ethtool_evpn_uplink_tarcking(self): + def test_ethtool_evpn_uplink_tracking(self): for interface in self._interfaces: self.cli_set(self._base_path + [interface, 'evpn', 'uplink']) self.cli_commit() for interface in self._interfaces: - frrconfig = self.getFRRconfig(f'interface {interface}', daemon='zebra') - self.assertIn(f' evpn mh uplink', frrconfig) + frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') + self.assertIn(' evpn mh uplink', frrconfig) + + def test_switchdev(self): + interface = self._interfaces[0] + self.cli_set(self._base_path + [interface, 'switchdev']) + + # check validate() - virtual interfaces do not support switchdev + # should print out warning that enabling failed + + self.cli_delete(self._base_path + [interface, 'switchdev']) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_loopback.py b/smoketest/scripts/cli/test_interfaces_loopback.py index 0454dc658..f4b6038c5 100755 --- a/smoketest/scripts/cli/test_interfaces_loopback.py +++ b/smoketest/scripts/cli/test_interfaces_loopback.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2023 VyOS maintainers and contributors +# Copyright (C) 2020-2025 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 @@ -17,6 +17,7 @@ import unittest from base_interfaces_test import BasicInterfaceTest +from base_interfaces_test import MSG_TESTCASE_UNSUPPORTED from netifaces import interfaces from vyos.utils.network import is_intf_addr_assigned @@ -53,7 +54,7 @@ class LoopbackInterfaceTest(BasicInterfaceTest.TestCase): self.assertTrue(is_intf_addr_assigned('lo', addr)) def test_interface_disable(self): - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_virtual-ethernet.py b/smoketest/scripts/cli/test_interfaces_virtual-ethernet.py index c6a4613a7..b2af86139 100755 --- a/smoketest/scripts/cli/test_interfaces_virtual-ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_virtual-ethernet.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2024 VyOS maintainers and contributors +# Copyright (C) 2023-2025 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 @@ -34,9 +34,6 @@ class VEthInterfaceTest(BasicInterfaceTest.TestCase): # call base-classes classmethod super(VEthInterfaceTest, cls).setUpClass() - def test_vif_8021q_mtu_limits(self): - self.skipTest('not supported') - # As we always need a pair of veth interfaces, we can not rely on the base # class check to determine if there is a dhcp6c or dhclient instance running. # This test will always fail as there is an instance running on the peer diff --git a/smoketest/scripts/cli/test_interfaces_vxlan.py b/smoketest/scripts/cli/test_interfaces_vxlan.py index b2076b43b..132496124 100755 --- a/smoketest/scripts/cli/test_interfaces_vxlan.py +++ b/smoketest/scripts/cli/test_interfaces_vxlan.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2023 VyOS maintainers and contributors +# Copyright (C) 2020-2025 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 @@ -114,6 +114,30 @@ class VXLANInterfaceTest(BasicInterfaceTest.TestCase): self.assertEqual(Interface(interface).get_admin_state(), 'up') ttl += 10 + + def test_vxlan_group_remote_error(self): + intf = 'vxlan60' + options = [ + 'group 239.4.4.5', + 'mtu 1420', + 'remote 192.168.0.254', + 'source-address 192.168.0.1', + 'source-interface eth0', + 'vni 60' + ] + for option in options: + opts = option.split() + self.cli_set(self._base_path + [intf] + opts) + + # verify() - Both group and remote cannot be specified + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + # Remove blocking CLI option + self.cli_delete(self._base_path + [intf, 'group']) + self.cli_commit() + + def test_vxlan_external(self): interface = 'vxlan0' source_address = '192.0.2.1' diff --git a/smoketest/scripts/cli/test_interfaces_wireguard.py b/smoketest/scripts/cli/test_interfaces_wireguard.py index 4b994a659..f8cd18cf2 100755 --- a/smoketest/scripts/cli/test_interfaces_wireguard.py +++ b/smoketest/scripts/cli/test_interfaces_wireguard.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2023 VyOS maintainers and contributors +# Copyright (C) 2020-2025 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 @@ -17,27 +17,33 @@ import os import unittest -from base_vyostest_shim import VyOSUnitTestSHIM +from base_interfaces_test import BasicInterfaceTest from vyos.configsession import ConfigSessionError from vyos.utils.file import read_file from vyos.utils.process import cmd +from vyos.utils.process import is_systemd_service_running base_path = ['interfaces', 'wireguard'] - -class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase): +domain_resolver = 'vyos-domain-resolver.service' +class WireGuardInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): - super(WireGuardInterfaceTest, cls).setUpClass() - - cls._test_addr = ['192.0.2.1/26', '192.0.2.255/31', '192.0.2.64/32', - '2001:db8:1::ffff/64', '2001:db8:101::1/112'] - cls._interfaces = ['wg0', 'wg1'] + cls._base_path = ['interfaces', 'wireguard'] + cls._options = { + 'wg0': ['private-key wBbGJJXYllwDcw63AFjiIR6ZlsvqvAf3eDwog64Dp0Q=', + 'peer RED public-key 6hkkfxN4VUQLu36NLZr47I7ST/FkQl2clPWr+9a6ZH8=', + 'peer RED allowed-ips 169.254.0.0/16', + 'port 5678'], + 'wg1': ['private-key QFwnBHlHYspehvpklBKb7cikM+QMkEy2p6gfsg06S08=', + 'peer BLUE public-key hRJLmP8SVU9/MLmPmYmpOa+RTB4F/zhDqA+/QDuW1Hg=', + 'peer BLUE allowed-ips 169.254.0.0/16', + 'port 4567'], + } + cls._interfaces = list(cls._options) - def tearDown(self): - self.cli_delete(base_path) - self.cli_commit() + super(WireGuardInterfaceTest, cls).setUpClass() - def test_01_wireguard_peer(self): + def test_wireguard_peer(self): # Create WireGuard interfaces with associated peers for intf in self._interfaces: peer = 'foo-' + intf @@ -64,7 +70,7 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase): self.assertTrue(os.path.isdir(f'/sys/class/net/{intf}')) - def test_02_wireguard_add_remove_peer(self): + def test_wireguard_add_remove_peer(self): # T2939: Create WireGuard interfaces with associated peers. # Remove one of the configured peers. # T4774: Test prevention of duplicate peer public keys @@ -102,7 +108,7 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path + [interface, 'peer', 'PEER01']) self.cli_commit() - def test_03_wireguard_same_public_key(self): + def test_wireguard_same_public_key(self): # T5413: Test prevention of equality interface public key and peer's # public key interface = 'wg0' @@ -115,45 +121,40 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + [interface, 'private-key', privkey]) self.cli_set(base_path + [interface, 'peer', 'PEER01', 'public-key', pubkey_fail]) - self.cli_set(base_path + [interface, 'peer', 'PEER01', 'port', port]) self.cli_set(base_path + [interface, 'peer', 'PEER01', 'allowed-ips', '10.205.212.10/32']) self.cli_set(base_path + [interface, 'peer', 'PEER01', 'address', '192.0.2.1']) # The same pubkey as the interface wg0 with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(base_path + [interface, 'peer', 'PEER01', 'public-key', pubkey_ok]) + # If address is defined for a peer, so must be the peer port + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + [interface, 'peer', 'PEER01', 'port', port]) + # Commit peers self.cli_commit() self.assertTrue(os.path.isdir(f'/sys/class/net/{interface}')) - def test_04_wireguard_threaded(self): + def test_wireguard_threaded(self): # T5409: Test adding threaded option on interface. - # Test prevention for adding threaded - # if no enabled peer is configured. - interface = 'wg0' - port = '12345' - privkey = 'OOjcXGfgQlAuM6q8Z9aAYduCua7pxf7UKYvIqoUPoGQ=' - pubkey = 'ebFx/1G0ti8tvuZd94sEIosAZZIznX+dBAKG/8DFm0I=' + for intf in self._interfaces: + for option in self._options.get(intf, []): + self.cli_set(self._base_path + [intf] + option.split()) - self.cli_set(base_path + [interface, 'address', '172.16.0.1/24']) - self.cli_set(base_path + [interface, 'private-key', privkey]) - - self.cli_set(base_path + [interface, 'peer', 'PEER01', 'port', port]) - self.cli_set(base_path + [interface, 'peer', 'PEER01', 'public-key', pubkey]) - self.cli_set(base_path + [interface, 'peer', 'PEER01', 'allowed-ips', '10.205.212.10/32']) - self.cli_set(base_path + [interface, 'peer', 'PEER01', 'address', '192.0.2.1']) - self.cli_set(base_path + [interface, 'per-client-thread']) + self.cli_set(base_path + [intf, 'per-client-thread']) # Commit peers self.cli_commit() - tmp = read_file(f'/sys/class/net/{interface}/threaded') - self.assertTrue(tmp, "1") - def test_05_wireguard_peer_pubkey_change(self): + for intf in self._interfaces: + tmp = read_file(f'/sys/class/net/{intf}/threaded') + self.assertTrue(tmp, "1") + + def test_wireguard_peer_pubkey_change(self): # T5707 changing WireGuard CLI public key of a peer - it's not removed def get_peers(interface) -> list: @@ -171,7 +172,6 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase): peers.append(items[0]) return peers - interface = 'wg1337' port = '1337' privkey = 'iJi4lb2HhkLx2KSAGOjji2alKkYsJjSPkHkrcpxgEVU=' @@ -200,5 +200,41 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase): self.assertNotIn(pubkey_1, peers) self.assertIn(pubkey_2, peers) + def test_wireguard_hostname(self): + # T4930: Test dynamic endpoint support + interface = 'wg1234' + port = '54321' + privkey = 'UOWIeZKNzijhgu0bPRy2PB3gnuOBLfQax5GiYfkmU3A=' + pubkey = '4nG5NfhHBQUq/DnwT0RjRoBCqAh3VrRHqdQgzC/xujk=' + + base_interface_path = base_path + [interface] + self.cli_set(base_interface_path + ['address', '172.16.0.1/24']) + self.cli_set(base_interface_path + ['private-key', privkey]) + + peer_base_path = base_interface_path + ['peer', 'dynamic01'] + self.cli_set(peer_base_path + ['port', port]) + self.cli_set(peer_base_path + ['public-key', pubkey]) + self.cli_set(peer_base_path + ['allowed-ips', '169.254.0.0/16']) + self.cli_set(peer_base_path + ['address', '192.0.2.1']) + self.cli_set(peer_base_path + ['host-name', 'wg.vyos.net']) + + # Peer address and host-name are mutually exclusive + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete(peer_base_path + ['address']) + + # Commit peers + self.cli_commit() + + # Ensure the service is running which checks for DNS changes + self.assertTrue(is_systemd_service_running(domain_resolver)) + + self.cli_delete(base_interface_path) + self.cli_commit() + + # Ensure the service is no longer running after WireGuard interface is deleted + self.assertFalse(is_systemd_service_running(domain_resolver)) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_wireless.py b/smoketest/scripts/cli/test_interfaces_wireless.py index b8b18f30f..1c69c1be5 100755 --- a/smoketest/scripts/cli/test_interfaces_wireless.py +++ b/smoketest/scripts/cli/test_interfaces_wireless.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright (C) 2020-2025 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 @@ -64,13 +64,23 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): # call base-classes classmethod super(WirelessInterfaceTest, cls).setUpClass() - # T5245 - currently testcases are disabled - cls._test_ipv6 = False - cls._test_vlan = False + # If any wireless interface is based on mac80211_hwsim, disable all + # VLAN related testcases. See T5245, T7325 + tmp = read_file('/proc/modules') + if 'mac80211_hwsim' in tmp: + cls._test_ipv6 = False + cls._test_vlan = False + cls._test_qinq = False + + # Loading mac80211_hwsim module created two WIFI Interfaces in the + # background (wlan0 and wlan1), remove them to have a clean test start. + # This must happen AFTER the above check for unsupported drivers + for interface in cls._interfaces: + if interface_exists(interface): + call(f'sudo iw dev {interface} del') cls.cli_set(cls, wifi_cc_path + [country]) - def test_wireless_add_single_ip_address(self): # derived method to check if member interfaces are enslaved properly super().test_add_single_ip_address() @@ -627,9 +637,4 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): if __name__ == '__main__': check_kmod('mac80211_hwsim') - # loading the module created two WIFI Interfaces in the background (wlan0 and wlan1) - # remove them to have a clean test start - for interface in ['wlan0', 'wlan1']: - if interface_exists(interface): - call(f'sudo iw dev {interface} del') unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py b/smoketest/scripts/cli/test_load-balancing_haproxy.py index 34f77b95d..833e0a92b 100755 --- a/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py +++ b/smoketest/scripts/cli/test_load-balancing_haproxy.py @@ -14,17 +14,20 @@ # 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 re +import textwrap import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError +from vyos.template import get_default_port from vyos.utils.process import process_named_running from vyos.utils.file import read_file PROCESS_NAME = 'haproxy' HAPROXY_CONF = '/run/haproxy/haproxy.cfg' -base_path = ['load-balancing', 'reverse-proxy'] +base_path = ['load-balancing', 'haproxy'] proxy_interface = 'eth1' valid_ca_cert = """ @@ -131,7 +134,25 @@ ZXLrtgVJR9W020qTurO2f91qfU8646n11hR9ObBB1IYbagOU0Pw1Nrq/FRp/u2tx 7i7xFz2WEiQeSCPaKYOiqM3t """ +haproxy_service_name = 'https_front' +haproxy_backend_name = 'bk-01' +def parse_haproxy_config() -> dict: + config_str = read_file(HAPROXY_CONF) + section_pattern = re.compile(r'^(global|defaults|frontend\s+\S+|backend\s+\S+)', re.MULTILINE) + sections = {} + + matches = list(section_pattern.finditer(config_str)) + + for i, match in enumerate(matches): + section_name = match.group(1).strip() + start = match.end() + end = matches[i + 1].start() if i + 1 < len(matches) else len(config_str) + section_body = config_str[start:end] + dedented_body = textwrap.dedent(section_body).strip() + sections[section_name] = dedented_body + + return sections class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): def tearDown(self): # Check for running process @@ -146,14 +167,14 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.assertFalse(process_named_running(PROCESS_NAME)) def base_config(self): - self.cli_set(base_path + ['service', 'https_front', 'mode', 'http']) - self.cli_set(base_path + ['service', 'https_front', 'port', '4433']) - self.cli_set(base_path + ['service', 'https_front', 'backend', 'bk-01']) + self.cli_set(base_path + ['service', haproxy_service_name, 'mode', 'http']) + self.cli_set(base_path + ['service', haproxy_service_name, 'port', '4433']) + self.cli_set(base_path + ['service', haproxy_service_name, 'backend', haproxy_backend_name]) - self.cli_set(base_path + ['backend', 'bk-01', 'mode', 'http']) - self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'address', '192.0.2.11']) - self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'port', '9090']) - self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'send-proxy']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'mode', 'http']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'server', haproxy_backend_name, 'address', '192.0.2.11']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'server', haproxy_backend_name, 'port', '9090']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'server', haproxy_backend_name, 'send-proxy']) self.cli_set(base_path + ['global-parameters', 'max-connections', '1000']) @@ -167,15 +188,15 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.cli_set(['pki', 'certificate', 'smoketest', 'certificate', valid_cert.replace('\n','')]) self.cli_set(['pki', 'certificate', 'smoketest', 'private', 'key', valid_cert_private_key.replace('\n','')]) - def test_01_lb_reverse_proxy_domain(self): + def test_reverse_proxy_domain(self): domains_bk_first = ['n1.example.com', 'n2.example.com', 'n3.example.com'] domain_bk_second = 'n5.example.com' - frontend = 'https_front' + frontend = 'vyos_smoketest' front_port = '4433' bk_server_first = '192.0.2.11' bk_server_second = '192.0.2.12' - bk_first_name = 'bk-01' - bk_second_name = 'bk-02' + bk_first_name = 'vyosbk-01' + bk_second_name = 'vyosbk-02' bk_server_port = '9090' mode = 'http' rule_ten = '10' @@ -241,9 +262,9 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.assertIn(f'server {bk_second_name} {bk_server_second}:{bk_server_port}', config) self.assertIn(f'server {bk_second_name} {bk_server_second}:{bk_server_port} backup', config) - def test_02_lb_reverse_proxy_cert_not_exists(self): + def test_reverse_proxy_cert_not_exists(self): self.base_config() - self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert']) + self.cli_set(base_path + ['service', haproxy_service_name, 'ssl', 'certificate', 'cert']) with self.assertRaises(ConfigSessionError) as e: self.cli_commit() @@ -253,19 +274,19 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.configure_pki() self.base_config() - self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert']) + self.cli_set(base_path + ['service', haproxy_service_name, 'ssl', 'certificate', 'cert']) with self.assertRaises(ConfigSessionError) as e: self.cli_commit() # self.assertIn('\nCertificate "cert" does not exist\n', str(e.exception)) - self.cli_delete(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert']) - self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'smoketest']) + self.cli_delete(base_path + ['service', haproxy_service_name, 'ssl', 'certificate', 'cert']) + self.cli_set(base_path + ['service', haproxy_service_name, 'ssl', 'certificate', 'smoketest']) self.cli_commit() - def test_03_lb_reverse_proxy_ca_not_exists(self): + def test_reverse_proxy_ca_not_exists(self): self.base_config() - self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'ssl', 'ca-certificate', 'ca-test']) with self.assertRaises(ConfigSessionError) as e: self.cli_commit() @@ -275,40 +296,40 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.configure_pki() self.base_config() - self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'ssl', 'ca-certificate', 'ca-test']) with self.assertRaises(ConfigSessionError) as e: self.cli_commit() # self.assertIn('\nCA certificate "ca-test" does not exist\n', str(e.exception)) - self.cli_delete(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test']) - self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'smoketest']) + self.cli_delete(base_path + ['backend', haproxy_backend_name, 'ssl', 'ca-certificate', 'ca-test']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'ssl', 'ca-certificate', 'smoketest']) self.cli_commit() - def test_04_lb_reverse_proxy_backend_ssl_no_verify(self): + def test_reverse_proxy_backend_ssl_no_verify(self): # Setup base self.configure_pki() self.base_config() # Set no-verify option - self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'no-verify']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'ssl', 'no-verify']) self.cli_commit() # Test no-verify option config = read_file(HAPROXY_CONF) - self.assertIn('server bk-01 192.0.2.11:9090 send-proxy ssl verify none', config) + self.assertIn(f'server {haproxy_backend_name} 192.0.2.11:9090 send-proxy ssl verify none', config) # Test setting ca-certificate alongside no-verify option fails, to test config validation - self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'smoketest']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'ssl', 'ca-certificate', 'smoketest']) with self.assertRaises(ConfigSessionError) as e: self.cli_commit() - def test_05_lb_reverse_proxy_backend_http_check(self): + def test_reverse_proxy_backend_http_check(self): # Setup base self.base_config() # Set http-check - self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'method', 'get']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'http-check', 'method', 'get']) self.cli_commit() # Test http-check @@ -317,8 +338,8 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.assertIn('http-check send meth GET', config) # Set http-check with uri and status - self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'uri', '/health']) - self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'status', '200']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'http-check', 'uri', '/health']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'http-check', 'expect', 'status', '200']) self.cli_commit() # Test http-check with uri and status @@ -328,8 +349,8 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.assertIn('http-check expect status 200', config) # Set http-check with string - self.cli_delete(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'status', '200']) - self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'string', 'success']) + self.cli_delete(base_path + ['backend', haproxy_backend_name, 'http-check', 'expect', 'status', '200']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'http-check', 'expect', 'string', 'success']) self.cli_commit() # Test http-check with string @@ -339,11 +360,11 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.assertIn('http-check expect string success', config) # Test configuring both http-check & health-check fails validation script - self.cli_set(base_path + ['backend', 'bk-01', 'health-check', 'ldap']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'health-check', 'ldap']) with self.assertRaises(ConfigSessionError) as e: self.cli_commit() - def test_06_lb_reverse_proxy_tcp_mode(self): + def test_reverse_proxy_tcp_mode(self): frontend = 'tcp_8443' mode = 'tcp' front_port = '8433' @@ -390,27 +411,27 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.assertIn(f'mode {mode}', config) self.assertIn(f'server {bk_name} {bk_server}:{bk_server_port}', config) - def test_07_lb_reverse_proxy_http_response_headers(self): + def test_reverse_proxy_http_response_headers(self): # Setup base self.configure_pki() self.base_config() # Set example headers in both frontend and backend - self.cli_set(base_path + ['service', 'https_front', 'http-response-headers', 'Cache-Control', 'value', 'max-age=604800']) - self.cli_set(base_path + ['backend', 'bk-01', 'http-response-headers', 'Proxy-Backend-ID', 'value', 'bk-01']) + self.cli_set(base_path + ['service', haproxy_service_name, 'http-response-headers', 'Cache-Control', 'value', 'max-age=604800']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'http-response-headers', 'Proxy-Backend-ID', 'value', haproxy_backend_name]) self.cli_commit() # Test headers are present in generated configuration file config = read_file(HAPROXY_CONF) self.assertIn('http-response set-header Cache-Control \'max-age=604800\'', config) - self.assertIn('http-response set-header Proxy-Backend-ID \'bk-01\'', config) + self.assertIn(f'http-response set-header Proxy-Backend-ID \'{haproxy_backend_name}\'', config) # Test setting alongside modes other than http is blocked by validation conditions - self.cli_set(base_path + ['service', 'https_front', 'mode', 'tcp']) + self.cli_set(base_path + ['service', haproxy_service_name, 'mode', 'tcp']) with self.assertRaises(ConfigSessionError) as e: self.cli_commit() - def test_08_lb_reverse_proxy_tcp_health_checks(self): + def test_reverse_proxy_tcp_health_checks(self): # Setup PKI self.configure_pki() @@ -458,7 +479,7 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): config = read_file(HAPROXY_CONF) self.assertIn(f'option smtpchk', config) - def test_09_lb_reverse_proxy_logging(self): + def test_reverse_proxy_logging(self): # Setup base self.base_config() self.cli_commit() @@ -477,7 +498,7 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.assertIn('log /dev/log local2 warning', config) # Test backend logging options - backend_path = base_path + ['backend', 'bk-01'] + backend_path = base_path + ['backend', haproxy_backend_name] self.cli_set(backend_path + ['logging', 'facility', 'local3', 'level', 'debug']) self.cli_set(backend_path + ['logging', 'facility', 'local4', 'level', 'info']) self.cli_commit() @@ -488,7 +509,7 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.assertIn('log /dev/log local4 info', config) # Test service logging options - service_path = base_path + ['service', 'https_front'] + service_path = base_path + ['service', haproxy_service_name] self.cli_set(service_path + ['logging', 'facility', 'local5', 'level', 'notice']) self.cli_set(service_path + ['logging', 'facility', 'local6', 'level', 'crit']) self.cli_commit() @@ -498,5 +519,97 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.assertIn('log /dev/log local5 notice', config) self.assertIn('log /dev/log local6 crit', config) + def test_reverse_proxy_http_compression(self): + # Setup base + self.configure_pki() + self.base_config() + + # Configure compression in frontend + http_comp_path = base_path + ['service', haproxy_service_name, 'http-compression'] + self.cli_set(http_comp_path + ['algorithm', 'gzip']) + self.cli_set(http_comp_path + ['mime-type', 'text/html']) + self.cli_set(http_comp_path + ['mime-type', 'text/javascript']) + self.cli_set(http_comp_path + ['mime-type', 'text/plain']) + self.cli_commit() + + # Test compression is present in generated configuration file + config = read_file(HAPROXY_CONF) + self.assertIn('filter compression', config) + self.assertIn('compression algo gzip', config) + self.assertIn('compression type text/html text/javascript text/plain', config) + + # Test setting compression without specifying any mime-types fails verification + self.cli_delete(base_path + ['service', haproxy_service_name, 'http-compression', 'mime-type']) + with self.assertRaises(ConfigSessionError) as e: + self.cli_commit() + + def test_reverse_proxy_timeout(self): + t_default_check = '5' + t_default_client = '50' + t_default_connect = '10' + t_default_server ='50' + t_check = '4' + t_client = '300' + t_connect = '12' + t_server ='120' + t_front_client = '600' + + self.base_config() + self.cli_commit() + # Check default timeout options + config_entries = ( + f'timeout check {t_default_check}s', + f'timeout connect {t_default_connect}s', + f'timeout client {t_default_client}s', + f'timeout server {t_default_server}s', + ) + # Check default timeout options + config = read_file(HAPROXY_CONF) + for config_entry in config_entries: + self.assertIn(config_entry, config) + + # Set custom timeout options + self.cli_set(base_path + ['timeout', 'check', t_check]) + self.cli_set(base_path + ['timeout', 'client', t_client]) + self.cli_set(base_path + ['timeout', 'connect', t_connect]) + self.cli_set(base_path + ['timeout', 'server', t_server]) + self.cli_set(base_path + ['service', haproxy_service_name, 'timeout', 'client', t_front_client]) + + self.cli_commit() + + # Check custom timeout options + config_entries = ( + f'timeout check {t_check}s', + f'timeout connect {t_connect}s', + f'timeout client {t_client}s', + f'timeout server {t_server}s', + f'timeout client {t_front_client}s', + ) + + # Check configured options + config = read_file(HAPROXY_CONF) + for config_entry in config_entries: + self.assertIn(config_entry, config) + + def test_reverse_proxy_http_redirect(self): + self.base_config() + self.cli_set(base_path + ['service', haproxy_service_name, 'redirect-http-to-https']) + + self.cli_commit() + + config = parse_haproxy_config() + frontend_name = f'frontend {haproxy_service_name}-http' + self.assertIn(frontend_name, config.keys()) + self.assertIn('mode http', config[frontend_name]) + self.assertIn('bind [::]:80 v4v6', config[frontend_name]) + self.assertIn('acl acme_acl path_beg /.well-known/acme-challenge/', config[frontend_name]) + self.assertIn('use_backend buildin_acme_certbot if acme_acl', config[frontend_name]) + self.assertIn('redirect scheme https code 301 if !acme_acl', config[frontend_name]) + + backend_name = 'backend buildin_acme_certbot' + self.assertIn(backend_name, config.keys()) + port = get_default_port('certbot_haproxy') + self.assertIn(f'server localhost 127.0.0.1:{port}', config[backend_name]) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_load-balancing_wan.py b/smoketest/scripts/cli/test_load-balancing_wan.py index 92b4000b8..32e5f6915 100755 --- a/smoketest/scripts/cli/test_load-balancing_wan.py +++ b/smoketest/scripts/cli/test_load-balancing_wan.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022-2024 VyOS maintainers and contributors +# Copyright (C) 2022-2025 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 @@ -14,10 +14,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import os import unittest import time from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.utils.file import chmod_755 +from vyos.utils.file import write_file from vyos.utils.process import call from vyos.utils.process import cmd @@ -54,6 +57,16 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() + removed_chains = [ + 'wlb_mangle_isp_veth1', + 'wlb_mangle_isp_veth2', + 'wlb_mangle_isp_eth201', + 'wlb_mangle_isp_eth202' + ] + + for chain in removed_chains: + self.verify_nftables_chain_exists('ip vyos_wanloadbalance', chain, inverse=True) + def test_table_routes(self): ns1 = 'ns201' ns2 = 'ns202' @@ -93,6 +106,7 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): cmd_in_netns(ns3, 'ip link set dev eth0 up') # Set load-balancing configuration + self.cli_set(base_path + ['wan', 'hook', '/bin/true']) self.cli_set(base_path + ['wan', 'interface-health', iface1, 'failure-count', '2']) self.cli_set(base_path + ['wan', 'interface-health', iface1, 'nexthop', '203.0.113.1']) self.cli_set(base_path + ['wan', 'interface-health', iface1, 'success-count', '1']) @@ -102,7 +116,8 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['wan', 'rule', '10', 'inbound-interface', iface3]) self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'address', '198.51.100.0/24']) - + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface1]) + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface2]) # commit changes self.cli_commit() @@ -127,7 +142,6 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): delete_netns(ns3) def test_check_chains(self): - ns1 = 'nsA' ns2 = 'nsB' ns3 = 'nsC' @@ -137,43 +151,28 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): container_iface1 = 'ceth0' container_iface2 = 'ceth1' container_iface3 = 'ceth2' - mangle_isp1 = """table ip mangle { - chain ISP_veth1 { - counter ct mark set 0xc9 - counter meta mark set 0xc9 - counter accept + mangle_isp1 = """table ip vyos_wanloadbalance { + chain wlb_mangle_isp_veth1 { + meta mark set 0x000000c9 ct mark set 0x000000c9 counter accept } }""" - mangle_isp2 = """table ip mangle { - chain ISP_veth2 { - counter ct mark set 0xca - counter meta mark set 0xca - counter accept + mangle_isp2 = """table ip vyos_wanloadbalance { + chain wlb_mangle_isp_veth2 { + meta mark set 0x000000ca ct mark set 0x000000ca counter accept } }""" - mangle_prerouting = """table ip mangle { - chain PREROUTING { + mangle_prerouting = """table ip vyos_wanloadbalance { + chain wlb_mangle_prerouting { type filter hook prerouting priority mangle; policy accept; - counter jump WANLOADBALANCE_PRE - } -}""" - mangle_wanloadbalance_pre = """table ip mangle { - chain WANLOADBALANCE_PRE { - iifname "veth3" ip saddr 198.51.100.0/24 ct state new meta random & 2147483647 < 1073741824 counter jump ISP_veth1 - iifname "veth3" ip saddr 198.51.100.0/24 ct state new counter jump ISP_veth2 + iifname "veth3" ip saddr 198.51.100.0/24 ct state new limit rate 5/second burst 5 packets counter numgen random mod 11 vmap { 0 : jump wlb_mangle_isp_veth1, 1-10 : jump wlb_mangle_isp_veth2 } iifname "veth3" ip saddr 198.51.100.0/24 counter meta mark set ct mark } }""" - nat_wanloadbalance = """table ip nat { - chain WANLOADBALANCE { - ct mark 0xc9 counter snat to 203.0.113.10 - ct mark 0xca counter snat to 192.0.2.10 - } -}""" - nat_vyos_pre_snat_hook = """table ip nat { - chain VYOS_PRE_SNAT_HOOK { + nat_wanloadbalance = """table ip vyos_wanloadbalance { + chain wlb_nat_postrouting { type nat hook postrouting priority srcnat - 1; policy accept; - counter jump WANLOADBALANCE + ct mark 0x000000c9 counter snat to 203.0.113.10 + ct mark 0x000000ca counter snat to 192.0.2.10 } }""" @@ -214,7 +213,7 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['wan', 'rule', '10', 'inbound-interface', iface3]) self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'address', '198.51.100.0/24']) self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface1]) - self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface2]) + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface2, 'weight', '10']) # commit changes self.cli_commit() @@ -222,25 +221,19 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): time.sleep(5) # Check mangle chains - tmp = cmd(f'sudo nft -s list chain mangle ISP_{iface1}') + tmp = cmd(f'sudo nft -s list chain ip vyos_wanloadbalance wlb_mangle_isp_{iface1}') self.assertEqual(tmp, mangle_isp1) - tmp = cmd(f'sudo nft -s list chain mangle ISP_{iface2}') + tmp = cmd(f'sudo nft -s list chain ip vyos_wanloadbalance wlb_mangle_isp_{iface2}') self.assertEqual(tmp, mangle_isp2) - tmp = cmd(f'sudo nft -s list chain mangle PREROUTING') + tmp = cmd('sudo nft -s list chain ip vyos_wanloadbalance wlb_mangle_prerouting') self.assertEqual(tmp, mangle_prerouting) - tmp = cmd(f'sudo nft -s list chain mangle WANLOADBALANCE_PRE') - self.assertEqual(tmp, mangle_wanloadbalance_pre) - # Check nat chains - tmp = cmd(f'sudo nft -s list chain nat WANLOADBALANCE') + tmp = cmd('sudo nft -s list chain ip vyos_wanloadbalance wlb_nat_postrouting') self.assertEqual(tmp, nat_wanloadbalance) - tmp = cmd(f'sudo nft -s list chain nat VYOS_PRE_SNAT_HOOK') - self.assertEqual(tmp, nat_vyos_pre_snat_hook) - # Delete veth interfaces and netns for iface in [iface1, iface2, iface3]: call(f'sudo ip link del dev {iface}') @@ -249,6 +242,85 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): delete_netns(ns2) delete_netns(ns3) + def test_criteria_failover_hook(self): + isp1_iface = 'eth0' + isp2_iface = 'eth1' + lan_iface = 'eth2' + + hook_path = '/tmp/wlb_hook.sh' + hook_output_path = '/tmp/wlb_hook_output' + hook_script = f""" +#!/bin/sh + +ifname=$WLB_INTERFACE_NAME +state=$WLB_INTERFACE_STATE + +echo "$ifname - $state" > {hook_output_path} +""" + + write_file(hook_path, hook_script) + chmod_755(hook_path) + + self.cli_set(['interfaces', 'ethernet', isp1_iface, 'address', '203.0.113.2/30']) + self.cli_set(['interfaces', 'ethernet', isp2_iface, 'address', '192.0.2.2/30']) + self.cli_set(['interfaces', 'ethernet', lan_iface, 'address', '198.51.100.2/30']) + + self.cli_set(base_path + ['wan', 'hook', hook_path]) + self.cli_set(base_path + ['wan', 'interface-health', isp1_iface, 'failure-count', '1']) + self.cli_set(base_path + ['wan', 'interface-health', isp1_iface, 'nexthop', '203.0.113.2']) + self.cli_set(base_path + ['wan', 'interface-health', isp1_iface, 'success-count', '1']) + self.cli_set(base_path + ['wan', 'interface-health', isp2_iface, 'failure-count', '1']) + self.cli_set(base_path + ['wan', 'interface-health', isp2_iface, 'nexthop', '192.0.2.2']) + self.cli_set(base_path + ['wan', 'interface-health', isp2_iface, 'success-count', '1']) + self.cli_set(base_path + ['wan', 'rule', '5', 'exclude']) + self.cli_set(base_path + ['wan', 'rule', '5', 'inbound-interface', 'eth*']) + self.cli_set(base_path + ['wan', 'rule', '5', 'destination', 'address', '10.0.0.0/8']) + self.cli_set(base_path + ['wan', 'rule', '10', 'failover']) + self.cli_set(base_path + ['wan', 'rule', '10', 'inbound-interface', lan_iface]) + self.cli_set(base_path + ['wan', 'rule', '10', 'protocol', 'udp']) + self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'address', '198.51.100.0/24']) + self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'port', '53']) + self.cli_set(base_path + ['wan', 'rule', '10', 'destination', 'address', '192.0.2.0/24']) + self.cli_set(base_path + ['wan', 'rule', '10', 'destination', 'port', '53']) + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', isp1_iface]) + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', isp1_iface, 'weight', '10']) + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', isp2_iface]) + + # commit changes + self.cli_commit() + + time.sleep(5) + + # Verify isp1 + criteria + + nftables_search = [ + [f'iifname "eth*"', 'ip daddr 10.0.0.0/8', 'return'], + [f'iifname "{lan_iface}"', 'ip saddr 198.51.100.0/24', 'udp sport 53', 'ip daddr 192.0.2.0/24', 'udp dport 53', f'jump wlb_mangle_isp_{isp1_iface}'] + ] + + self.verify_nftables_chain(nftables_search, 'ip vyos_wanloadbalance', 'wlb_mangle_prerouting') + + # Trigger failure on isp1 health check + + self.cli_delete(['interfaces', 'ethernet', isp1_iface, 'address', '203.0.113.2/30']) + self.cli_commit() + + time.sleep(10) + + # Verify failover to isp2 + + nftables_search = [ + [f'iifname "{lan_iface}"', f'jump wlb_mangle_isp_{isp2_iface}'] + ] + + self.verify_nftables_chain(nftables_search, 'ip vyos_wanloadbalance', 'wlb_mangle_prerouting') + + # Verify hook output + + self.assertTrue(os.path.exists(hook_output_path)) + + with open(hook_output_path, 'r') as f: + self.assertIn('eth0 - FAILED', f.read()) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_nat.py b/smoketest/scripts/cli/test_nat.py index 5161e47fd..b33ef2617 100755 --- a/smoketest/scripts/cli/test_nat.py +++ b/smoketest/scripts/cli/test_nat.py @@ -84,7 +84,7 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): address_group = 'smoketest_addr' address_group_member = '192.0.2.1' interface_group = 'smoketest_ifaces' - interface_group_member = 'bond.99' + interface_group_member = 'eth0' self.cli_set(['firewall', 'group', 'address-group', address_group, 'address', address_group_member]) self.cli_set(['firewall', 'group', 'interface-group', interface_group, 'interface', interface_group_member]) @@ -304,5 +304,31 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip vyos_nat') + def test_nat_fqdn(self): + source_domain = 'vyos.dev' + destination_domain = 'vyos.io' + + self.cli_set(src_path + ['rule', '1', 'outbound-interface', 'name', 'eth0']) + self.cli_set(src_path + ['rule', '1', 'source', 'fqdn', source_domain]) + self.cli_set(src_path + ['rule', '1', 'translation', 'address', 'masquerade']) + + self.cli_set(dst_path + ['rule', '1', 'destination', 'fqdn', destination_domain]) + self.cli_set(dst_path + ['rule', '1', 'source', 'fqdn', source_domain]) + self.cli_set(dst_path + ['rule', '1', 'destination', 'port', '5122']) + self.cli_set(dst_path + ['rule', '1', 'protocol', 'tcp']) + self.cli_set(dst_path + ['rule', '1', 'translation', 'address', '198.51.100.1']) + self.cli_set(dst_path + ['rule', '1', 'translation', 'port', '22']) + + + self.cli_commit() + + nftables_search = [ + ['set FQDN_nat_destination_1_d'], + ['set FQDN_nat_source_1_s'], + ['oifname "eth0"', 'ip saddr @FQDN_nat_source_1_s', 'masquerade', 'comment "SRC-NAT-1"'], + ['tcp dport 5122', 'ip saddr @FQDN_nat_destination_1_s', 'ip daddr @FQDN_nat_destination_1_d', 'dnat to 198.51.100.1:22', 'comment "DST-NAT-1"'] + ] + + self.verify_nftables(nftables_search, 'ip vyos_nat') if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_nat66.py b/smoketest/scripts/cli/test_nat66.py index 52ad8e3ef..d4b5d6aa4 100755 --- a/smoketest/scripts/cli/test_nat66.py +++ b/smoketest/scripts/cli/test_nat66.py @@ -227,6 +227,35 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip6 vyos_nat') + def test_source_nat66_network_group(self): + address_group = 'smoketest_addr' + address_group_member = 'fc00::1' + network_group = 'smoketest_net' + network_group_member = 'fc00::/64' + translation_prefix = 'fc01::/64' + + self.cli_set(['firewall', 'group', 'ipv6-address-group', address_group, 'address', address_group_member]) + self.cli_set(['firewall', 'group', 'ipv6-network-group', network_group, 'network', network_group_member]) + + self.cli_set(src_path + ['rule', '1', 'destination', 'group', 'address-group', address_group]) + self.cli_set(src_path + ['rule', '1', 'translation', 'address', translation_prefix]) + + self.cli_set(src_path + ['rule', '2', 'destination', 'group', 'network-group', network_group]) + self.cli_set(src_path + ['rule', '2', 'translation', 'address', translation_prefix]) + + self.cli_commit() + + nftables_search = [ + [f'set A6_{address_group}'], + [f'elements = {{ {address_group_member} }}'], + [f'set N6_{network_group}'], + [f'elements = {{ {network_group_member} }}'], + ['ip6 daddr', f'@A6_{address_group}', 'snat prefix to fc01::/64'], + ['ip6 daddr', f'@N6_{network_group}', 'snat prefix to fc01::/64'] + ] + + self.verify_nftables(nftables_search, 'ip6 vyos_nat') + def test_nat66_no_rules(self): # T3206: deleting all rules but keep the direction 'destination' or # 'source' resulteds in KeyError: 'rule'. diff --git a/smoketest/scripts/cli/test_policy.py b/smoketest/scripts/cli/test_policy.py index a0c6ab055..985097726 100755 --- a/smoketest/scripts/cli/test_policy.py +++ b/smoketest/scripts/cli/test_policy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2025 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 @@ -17,6 +17,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.configsession import ConfigSessionError from vyos.utils.process import cmd @@ -24,6 +25,17 @@ from vyos.utils.process import cmd base_path = ['policy'] class TestPolicy(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(TestPolicy, cls).setUpClass() + + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + cls.cli_delete(cls, ['vrf']) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME + def tearDown(self): self.cli_delete(base_path) self.cli_commit() @@ -1137,6 +1149,16 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): }, }, }, + 'vrf-match': { + 'rule': { + '10': { + 'action': 'permit', + 'match': { + 'source-vrf': 'TEST', + }, + }, + }, + }, } self.cli_set(['policy', 'access-list', access_list, 'rule', '10', 'action', 'permit']) @@ -1248,6 +1270,8 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_set(path + ['rule', rule, 'match', 'rpki', 'valid']) if 'protocol' in rule_config['match']: self.cli_set(path + ['rule', rule, 'match', 'protocol', rule_config['match']['protocol']]) + if 'source-vrf' in rule_config['match']: + self.cli_set(path + ['rule', rule, 'match', 'source-vrf', rule_config['match']['source-vrf']]) if 'tag' in rule_config['match']: self.cli_set(path + ['rule', rule, 'match', 'tag', rule_config['match']['tag']]) @@ -1426,6 +1450,9 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): if 'rpki-valid' in rule_config['match']: tmp = f'match rpki valid' self.assertIn(tmp, config) + if 'source-vrf' in rule_config['match']: + tmp = f'match source-vrf {rule_config["match"]["source-vrf"]}' + self.assertIn(tmp, config) if 'tag' in rule_config['match']: tmp = f'match tag {rule_config["match"]["tag"]}' self.assertIn(tmp, config) @@ -1945,7 +1972,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): local_preference = base_local_preference table = base_table for route_map in route_maps: - config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='') + config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='', endsection='^exit') self.assertIn(f' set local-preference {local_preference}', config) self.assertIn(f' set table {table}', config) local_preference += 20 @@ -1958,7 +1985,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): local_preference = base_local_preference for route_map in route_maps: - config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='') + config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='', endsection='^exit') self.assertIn(f' set local-preference {local_preference}', config) local_preference += 20 @@ -1972,7 +1999,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_commit() for route_map in route_maps: - config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='') + config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='', endsection='^exit') self.assertIn(f' set as-path prepend {prepend}', config) for route_map in route_maps: @@ -1981,7 +2008,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_commit() for route_map in route_maps: - config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='') + config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='', endsection='^exit') self.assertNotIn(f' set', config) def sort_ip(output): diff --git a/smoketest/scripts/cli/test_policy_local-route.py b/smoketest/scripts/cli/test_policy_local-route.py new file mode 100644 index 000000000..a4239b8a1 --- /dev/null +++ b/smoketest/scripts/cli/test_policy_local-route.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024-2025 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 base_vyostest_shim import CSTORE_GUARD_TIME + +interface = 'eth0' +mark = '100' +table_id = '101' +extra_table_id = '102' +vrf_name = 'LPBRVRF' +vrf_rt_id = '202' + +class TestPolicyLocalRoute(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(TestPolicyLocalRoute, cls).setUpClass() + # Clear out current configuration to allow running this test on a live system + cls.cli_delete(cls, ['policy', 'local-route']) + cls.cli_delete(cls, ['policy', 'local-route6']) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME + + cls.cli_set(cls, ['vrf', 'name', vrf_name, 'table', vrf_rt_id]) + + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, ['vrf', 'name', vrf_name]) + + super(TestPolicyLocalRoute, cls).tearDownClass() + + def tearDown(self): + self.cli_delete(['policy', 'local-route']) + self.cli_delete(['policy', 'local-route6']) + self.cli_commit() + + ip_rule_search = [ + [f'lookup {table_id}'] + ] + + self.verify_rules(ip_rule_search, inverse=True) + self.verify_rules(ip_rule_search, inverse=True, addr_family='inet6') + + def test_local_pbr_matching_criteria(self): + self.cli_set(['policy', 'local-route', 'rule', '4', 'inbound-interface', interface]) + self.cli_set(['policy', 'local-route', 'rule', '4', 'protocol', 'udp']) + self.cli_set(['policy', 'local-route', 'rule', '4', 'fwmark', mark]) + self.cli_set(['policy', 'local-route', 'rule', '4', 'destination', 'address', '198.51.100.0/24']) + self.cli_set(['policy', 'local-route', 'rule', '4', 'destination', 'port', '111']) + self.cli_set(['policy', 'local-route', 'rule', '4', 'source', 'address', '198.51.100.1']) + self.cli_set(['policy', 'local-route', 'rule', '4', 'source', 'port', '443']) + self.cli_set(['policy', 'local-route', 'rule', '4', 'set', 'table', table_id]) + + self.cli_set(['policy', 'local-route6', 'rule', '6', 'inbound-interface', interface]) + self.cli_set(['policy', 'local-route6', 'rule', '6', 'protocol', 'tcp']) + self.cli_set(['policy', 'local-route6', 'rule', '6', 'fwmark', mark]) + self.cli_set(['policy', 'local-route6', 'rule', '6', 'destination', 'address', '2001:db8::/64']) + self.cli_set(['policy', 'local-route6', 'rule', '6', 'destination', 'port', '123']) + self.cli_set(['policy', 'local-route6', 'rule', '6', 'source', 'address', '2001:db8::1']) + self.cli_set(['policy', 'local-route6', 'rule', '6', 'source', 'port', '80']) + self.cli_set(['policy', 'local-route6', 'rule', '6', 'set', 'table', table_id]) + + self.cli_commit() + + rule_lookup = f'lookup {table_id}' + rule_fwmark = 'fwmark ' + hex(int(mark)) + rule_interface = f'iif {interface}' + + ip4_rule_search = [ + ['from 198.51.100.1', 'to 198.51.100.0/24', rule_fwmark, rule_interface, 'ipproto udp', 'sport 443', 'dport 111', rule_lookup] + ] + + self.verify_rules(ip4_rule_search) + + ip6_rule_search = [ + ['from 2001:db8::1', 'to 2001:db8::/64', rule_fwmark, rule_interface, 'ipproto tcp', 'sport 80', 'dport 123', rule_lookup] + ] + + self.verify_rules(ip6_rule_search, addr_family='inet6') + + def test_local_pbr_rule_removal(self): + self.cli_set(['policy', 'local-route', 'rule', '1', 'destination', 'address', '198.51.100.1']) + self.cli_set(['policy', 'local-route', 'rule', '1', 'set', 'table', table_id]) + + self.cli_set(['policy', 'local-route', 'rule', '2', 'destination', 'address', '198.51.100.2']) + self.cli_set(['policy', 'local-route', 'rule', '2', 'set', 'table', table_id]) + + self.cli_set(['policy', 'local-route', 'rule', '3', 'destination', 'address', '198.51.100.3']) + self.cli_set(['policy', 'local-route', 'rule', '3', 'set', 'table', table_id]) + + self.cli_commit() + + rule_lookup = f'lookup {table_id}' + + ip_rule_search = [ + ['to 198.51.100.1', rule_lookup], + ['to 198.51.100.2', rule_lookup], + ['to 198.51.100.3', rule_lookup], + ] + + self.verify_rules(ip_rule_search) + + self.cli_delete(['policy', 'local-route', 'rule', '2']) + self.cli_commit() + + ip_rule_missing = [ + ['to 198.51.100.2', rule_lookup], + ] + + self.verify_rules(ip_rule_missing, inverse=True) + + def test_local_pbr_rule_changes(self): + self.cli_set(['policy', 'local-route', 'rule', '1', 'destination', 'address', '198.51.100.0/24']) + self.cli_set(['policy', 'local-route', 'rule', '1', 'set', 'table', table_id]) + + self.cli_commit() + + self.cli_set(['policy', 'local-route', 'rule', '1', 'set', 'table', extra_table_id]) + self.cli_commit() + + ip_rule_search_extra = [ + ['to 198.51.100.0/24', f'lookup {extra_table_id}'] + ] + + self.verify_rules(ip_rule_search_extra) + + ip_rule_search_orig = [ + ['to 198.51.100.0/24', f'lookup {table_id}'] + ] + + self.verify_rules(ip_rule_search_orig, inverse=True) + + self.cli_delete(['policy', 'local-route', 'rule', '1', 'set', 'table']) + self.cli_set(['policy', 'local-route', 'rule', '1', 'set', 'vrf', vrf_name]) + + self.cli_commit() + + ip_rule_search_vrf = [ + ['to 198.51.100.0/24', f'lookup {vrf_name}'] + ] + + self.verify_rules(ip_rule_search_extra, inverse=True) + self.verify_rules(ip_rule_search_vrf) + + def test_local_pbr_target_vrf(self): + self.cli_set(['policy', 'local-route', 'rule', '1', 'destination', 'address', '198.51.100.0/24']) + self.cli_set(['policy', 'local-route', 'rule', '1', 'set', 'vrf', vrf_name]) + + self.cli_commit() + + ip_rule_search = [ + ['to 198.51.100.0/24', f'lookup {vrf_name}'] + ] + + self.verify_rules(ip_rule_search) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_policy_route.py b/smoketest/scripts/cli/test_policy_route.py index 797ab9770..15ddd857e 100755 --- a/smoketest/scripts/cli/test_policy_route.py +++ b/smoketest/scripts/cli/test_policy_route.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2025 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 @@ -17,8 +17,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM - -from vyos.utils.process import cmd +from base_vyostest_shim import CSTORE_GUARD_TIME mark = '100' conn_mark = '555' @@ -38,10 +37,12 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): # Clear out current configuration to allow running this test on a live system cls.cli_delete(cls, ['policy', 'route']) cls.cli_delete(cls, ['policy', 'route6']) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME cls.cli_set(cls, ['interfaces', 'ethernet', interface, 'address', interface_ip]) cls.cli_set(cls, ['protocols', 'static', 'table', table_id, 'route', '0.0.0.0/0', 'interface', interface]) - + cls.cli_set(cls, ['vrf', 'name', vrf, 'table', vrf_table_id]) @classmethod @@ -73,17 +74,6 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): self.verify_rules(ip_rule_search, inverse=True) - def verify_rules(self, rules_search, inverse=False): - rule_output = cmd('ip rule show') - - for search in rules_search: - matched = False - for line in rule_output.split("\n"): - if all(item in line for item in search): - matched = True - break - self.assertTrue(not matched if inverse else matched, msg=search) - def test_pbr_group(self): self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'network', '172.16.99.0/24']) self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network1', 'network', '172.16.101.0/24']) @@ -317,5 +307,39 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables6_search, 'ip6 vyos_mangle') + def test_geoip(self): + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'action', 'drop']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'source', 'geoip', 'country-code', 'se']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'source', 'geoip', 'country-code', 'gb']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '2', 'action', 'accept']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '2', 'source', 'geoip', 'country-code', 'de']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '2', 'source', 'geoip', 'country-code', 'fr']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '2', 'source', 'geoip', 'inverse-match']) + + self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'action', 'drop']) + self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'source', 'geoip', 'country-code', 'se']) + self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'source', 'geoip', 'country-code', 'gb']) + self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '2', 'action', 'accept']) + self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '2', 'source', 'geoip', 'country-code', 'de']) + self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '2', 'source', 'geoip', 'country-code', 'fr']) + self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '2', 'source', 'geoip', 'inverse-match']) + + self.cli_commit() + + nftables_search = [ + ['ip saddr @GEOIP_CC_route_smoketest_1', 'drop'], + ['ip saddr != @GEOIP_CC_route_smoketest_2', 'accept'], + ] + + # -t prevents 1000+ GeoIP elements being returned + self.verify_nftables(nftables_search, 'ip vyos_mangle', args='-t') + + nftables_search = [ + ['ip6 saddr @GEOIP_CC6_route6_smoketest6_1', 'drop'], + ['ip6 saddr != @GEOIP_CC6_route6_smoketest6_2', 'accept'], + ] + + self.verify_nftables(nftables_search, 'ip6 vyos_mangle', args='-t') + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_babel.py b/smoketest/scripts/cli/test_protocols_babel.py new file mode 100755 index 000000000..3a9ee2d62 --- /dev/null +++ b/smoketest/scripts/cli/test_protocols_babel.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024-2025 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 base_vyostest_shim import CSTORE_GUARD_TIME + +from vyos.ifconfig import Section +from vyos.frrender import babel_daemon +from vyos.utils.process import process_named_running +from vyos.xml_ref import default_value + +base_path = ['protocols', 'babel'] + +class TestProtocolsBABEL(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + cls._interfaces = Section.interfaces('ethernet', vlan=False) + # call base-classes classmethod + super(TestProtocolsBABEL, cls).setUpClass() + # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same + cls.daemon_pid = process_named_running(babel_daemon) + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + cls.cli_delete(cls, ['policy', 'prefix-list']) + cls.cli_delete(cls, ['policy', 'prefix-list6']) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME + + def tearDown(self): + # always destroy the entire babel configuration to make the processes + # life as hard as possible + self.cli_delete(base_path) + self.cli_delete(['policy', 'prefix-list']) + self.cli_delete(['policy', 'prefix-list6']) + self.cli_commit() + + # check process health and continuity + self.assertEqual(self.daemon_pid, process_named_running(babel_daemon)) + + def test_01_basic(self): + diversity_factor = '64' + resend_delay = '100' + smoothing_half_life = '400' + + self.cli_set(base_path + ['parameters', 'diversity']) + self.cli_set(base_path + ['parameters', 'diversity-factor', diversity_factor]) + self.cli_set(base_path + ['parameters', 'resend-delay', resend_delay]) + self.cli_set(base_path + ['parameters', 'smoothing-half-life', smoothing_half_life]) + + self.cli_commit() + + frrconfig = self.getFRRconfig('router babel', endsection='^exit') + self.assertIn(f' babel diversity', frrconfig) + self.assertIn(f' babel diversity-factor {diversity_factor}', frrconfig) + self.assertIn(f' babel resend-delay {resend_delay}', frrconfig) + self.assertIn(f' babel smoothing-half-life {smoothing_half_life}', frrconfig) + + def test_02_redistribute(self): + ipv4_protos = ['bgp', 'connected', 'isis', 'kernel', 'nhrp', 'ospf', 'rip', 'static'] + ipv6_protos = ['bgp', 'connected', 'isis', 'kernel', 'ospfv3', 'ripng', 'static'] + + self.cli_set(base_path + ['interface', self._interfaces[0], 'enable-timestamps']) + + for protocol in ipv4_protos: + self.cli_set(base_path + ['redistribute', 'ipv4', protocol]) + for protocol in ipv6_protos: + self.cli_set(base_path + ['redistribute', 'ipv6', protocol]) + + self.cli_commit() + + frrconfig = self.getFRRconfig('router babel', endsection='^exit', empty_retry=5) + for protocol in ipv4_protos: + self.assertIn(f' redistribute ipv4 {protocol}', frrconfig) + for protocol in ipv6_protos: + if protocol == 'ospfv3': + protocol = 'ospf6' + self.assertIn(f' redistribute ipv6 {protocol}', frrconfig) + + def test_03_distribute_list(self): + access_list_in4 = '40' + access_list_out4 = '50' + access_list_in4_iface = '44' + access_list_out4_iface = '55' + access_list_in6 = 'AL-foo-in6' + access_list_out6 = 'AL-foo-out6' + + prefix_list_in4 = 'PL-foo-in4' + prefix_list_out4 = 'PL-foo-out4' + prefix_list_in6 = 'PL-foo-in6' + prefix_list_out6 = 'PL-foo-out6' + + self.cli_set(['policy', 'access-list', access_list_in4]) + self.cli_set(['policy', 'access-list', access_list_out4]) + self.cli_set(['policy', 'access-list6', access_list_in6]) + self.cli_set(['policy', 'access-list6', access_list_out6]) + + self.cli_set(['policy', 'access-list', f'{access_list_in4_iface}']) + self.cli_set(['policy', 'access-list', f'{access_list_out4_iface}']) + + self.cli_set(['policy', 'prefix-list', prefix_list_in4]) + self.cli_set(['policy', 'prefix-list', prefix_list_out4]) + self.cli_set(['policy', 'prefix-list6', prefix_list_in6]) + self.cli_set(['policy', 'prefix-list6', prefix_list_out6]) + + self.cli_set(base_path + ['distribute-list', 'ipv4', 'access-list', 'in', access_list_in4]) + self.cli_set(base_path + ['distribute-list', 'ipv4', 'access-list', 'out', access_list_out4]) + self.cli_set(base_path + ['distribute-list', 'ipv6', 'access-list', 'in', access_list_in6]) + self.cli_set(base_path + ['distribute-list', 'ipv6', 'access-list', 'out', access_list_out6]) + + self.cli_set(base_path + ['distribute-list', 'ipv4', 'prefix-list', 'in', prefix_list_in4]) + self.cli_set(base_path + ['distribute-list', 'ipv4', 'prefix-list', 'out', prefix_list_out4]) + self.cli_set(base_path + ['distribute-list', 'ipv6', 'prefix-list', 'in', prefix_list_in6]) + self.cli_set(base_path + ['distribute-list', 'ipv6', 'prefix-list', 'out', prefix_list_out6]) + + for interface in self._interfaces: + self.cli_set(base_path + ['interface', interface]) + + self.cli_set(['policy', 'access-list6', f'{access_list_in6}-{interface}']) + self.cli_set(['policy', 'access-list6', f'{access_list_out6}-{interface}']) + + self.cli_set(['policy', 'prefix-list', f'{prefix_list_in4}-{interface}']) + self.cli_set(['policy', 'prefix-list', f'{prefix_list_out4}-{interface}']) + self.cli_set(['policy', 'prefix-list6', f'{prefix_list_in6}-{interface}']) + self.cli_set(['policy', 'prefix-list6', f'{prefix_list_out6}-{interface}']) + + tmp_path = base_path + ['distribute-list', 'ipv4', 'interface', interface] + self.cli_set(tmp_path + ['access-list', 'in', f'{access_list_in4_iface}']) + self.cli_set(tmp_path + ['access-list', 'out', f'{access_list_out4_iface}']) + self.cli_set(tmp_path + ['prefix-list', 'in', f'{prefix_list_in4}-{interface}']) + self.cli_set(tmp_path + ['prefix-list', 'out', f'{prefix_list_out4}-{interface}']) + + tmp_path = base_path + ['distribute-list', 'ipv6', 'interface', interface] + self.cli_set(tmp_path + ['access-list', 'in', f'{access_list_in6}-{interface}']) + self.cli_set(tmp_path + ['access-list', 'out', f'{access_list_out6}-{interface}']) + self.cli_set(tmp_path + ['prefix-list', 'in', f'{prefix_list_in6}-{interface}']) + self.cli_set(tmp_path + ['prefix-list', 'out', f'{prefix_list_out6}-{interface}']) + + self.cli_commit() + + frrconfig = self.getFRRconfig('router babel', endsection='^exit') + self.assertIn(f' distribute-list {access_list_in4} in', frrconfig) + self.assertIn(f' distribute-list {access_list_out4} out', frrconfig) + self.assertIn(f' ipv6 distribute-list {access_list_in6} in', frrconfig) + self.assertIn(f' ipv6 distribute-list {access_list_out6} out', frrconfig) + + self.assertIn(f' distribute-list prefix {prefix_list_in4} in', frrconfig) + self.assertIn(f' distribute-list prefix {prefix_list_out4} out', frrconfig) + self.assertIn(f' ipv6 distribute-list prefix {prefix_list_in6} in', frrconfig) + self.assertIn(f' ipv6 distribute-list prefix {prefix_list_out6} out', frrconfig) + + for interface in self._interfaces: + self.assertIn(f' distribute-list {access_list_in4_iface} in {interface}', frrconfig) + self.assertIn(f' distribute-list {access_list_out4_iface} out {interface}', frrconfig) + self.assertIn(f' ipv6 distribute-list {access_list_in6}-{interface} in {interface}', frrconfig) + self.assertIn(f' ipv6 distribute-list {access_list_out6}-{interface} out {interface}', frrconfig) + + self.assertIn(f' distribute-list prefix {prefix_list_in4}-{interface} in {interface}', frrconfig) + self.assertIn(f' distribute-list prefix {prefix_list_out4}-{interface} out {interface}', frrconfig) + self.assertIn(f' ipv6 distribute-list prefix {prefix_list_in6}-{interface} in {interface}', frrconfig) + self.assertIn(f' ipv6 distribute-list prefix {prefix_list_out6}-{interface} out {interface}', frrconfig) + + def test_04_interfaces(self): + def_update_interval = default_value(base_path + ['interface', 'eth0', 'update-interval']) + channel = '20' + hello_interval = '1000' + max_rtt_penalty = '100' + rtt_decay = '23' + rtt_max = '119' + rtt_min = '11' + rxcost = '40000' + type = 'wired' + + for interface in self._interfaces: + self.cli_set(base_path + ['interface', interface]) + self.cli_set(base_path + ['interface', interface, 'channel', channel]) + self.cli_set(base_path + ['interface', interface, 'enable-timestamps']) + self.cli_set(base_path + ['interface', interface, 'hello-interval', hello_interval]) + self.cli_set(base_path + ['interface', interface, 'max-rtt-penalty', max_rtt_penalty]) + self.cli_set(base_path + ['interface', interface, 'rtt-decay', rtt_decay]) + self.cli_set(base_path + ['interface', interface, 'rtt-max', rtt_max]) + self.cli_set(base_path + ['interface', interface, 'rtt-min', rtt_min]) + self.cli_set(base_path + ['interface', interface, 'rxcost', rxcost]) + self.cli_set(base_path + ['interface', interface, 'split-horizon', 'disable']) + self.cli_set(base_path + ['interface', interface, 'type', type]) + + self.cli_commit() + + frrconfig = self.getFRRconfig('router babel', endsection='^exit') + for interface in self._interfaces: + self.assertIn(f' network {interface}', frrconfig) + + iface_config = self.getFRRconfig(f'interface {interface}', endsection='^exit') + self.assertIn(f' babel channel {channel}', iface_config) + self.assertIn(f' babel enable-timestamps', iface_config) + self.assertIn(f' babel update-interval {def_update_interval}', iface_config) + self.assertIn(f' babel hello-interval {hello_interval}', iface_config) + self.assertIn(f' babel rtt-decay {rtt_decay}', iface_config) + self.assertIn(f' babel rtt-max {rtt_max}', iface_config) + self.assertIn(f' babel rtt-min {rtt_min}', iface_config) + self.assertIn(f' babel rxcost {rxcost}', iface_config) + self.assertIn(f' babel max-rtt-penalty {max_rtt_penalty}', iface_config) + self.assertIn(f' no babel split-horizon', iface_config) + self.assertIn(f' babel {type}', iface_config) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_bfd.py b/smoketest/scripts/cli/test_protocols_bfd.py index 716d0a806..2205cd9de 100755 --- a/smoketest/scripts/cli/test_protocols_bfd.py +++ b/smoketest/scripts/cli/test_protocols_bfd.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2024 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 @@ -17,10 +17,12 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME + from vyos.configsession import ConfigSessionError +from vyos.frrender import bfd_daemon from vyos.utils.process import process_named_running -PROCESS_NAME = 'bfdd' base_path = ['protocols', 'bfd'] dum_if = 'dum1001' @@ -84,7 +86,10 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): super(TestProtocolsBFD, cls).setUpClass() # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same - cls.daemon_pid = process_named_running(PROCESS_NAME) + cls.daemon_pid = process_named_running(bfd_daemon) + + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME # ensure we can also run this test on a live system - so lets clean # out the current configuration :) @@ -95,7 +100,7 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): self.cli_commit() # check process health and continuity - self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) + self.assertEqual(self.daemon_pid, process_named_running(bfd_daemon)) def test_bfd_peer(self): self.cli_set(['vrf', 'name', vrf_name, 'table', '1000']) @@ -130,7 +135,7 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig('bfd', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('bfd', endsection='^exit') for peer, peer_config in peers.items(): tmp = f'peer {peer}' if 'multihop' in peer_config: @@ -143,8 +148,8 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): tmp += f' vrf {peer_config["vrf"]}' self.assertIn(tmp, frrconfig) - peerconfig = self.getFRRconfig(f' peer {peer}', end='', daemon=PROCESS_NAME) - + peerconfig = self.getFRRconfig('bfd', endsection='^exit', substring=f' peer {peer}', + endsubsection='^ exit') if 'echo_mode' in peer_config: self.assertIn(f'echo-mode', peerconfig) if 'intv_echo' in peer_config: @@ -206,7 +211,8 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): # Verify FRR bgpd configuration for profile, profile_config in profiles.items(): - config = self.getFRRconfig(f' profile {profile}', endsection='^ !') + config = self.getFRRconfig('bfd', endsection='^exit', + substring=f' profile {profile}', endsubsection='^ exit',) if 'echo_mode' in profile_config: self.assertIn(f' echo-mode', config) if 'intv_echo' in profile_config: @@ -228,7 +234,8 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): self.assertNotIn(f'shutdown', config) for peer, peer_config in peers.items(): - peerconfig = self.getFRRconfig(f' peer {peer}', end='', daemon=PROCESS_NAME) + peerconfig = self.getFRRconfig('bfd', endsection='^exit', + substring=f' peer {peer}', endsubsection='^ exit') if 'profile' in peer_config: self.assertIn(f' profile {peer_config["profile"]}', peerconfig) diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index ea2f561a4..8403dcc37 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2025 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 @@ -19,14 +19,15 @@ import unittest from time import sleep from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.ifconfig import Section from vyos.configsession import ConfigSessionError from vyos.template import is_ipv6 from vyos.utils.process import process_named_running from vyos.utils.process import cmd +from vyos.frrender import bgp_daemon -PROCESS_NAME = 'bgpd' ASN = '64512' base_path = ['protocols', 'bgp'] @@ -178,7 +179,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): super(TestProtocolsBGP, cls).setUpClass() # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same - cls.daemon_pid = process_named_running(PROCESS_NAME) + cls.daemon_pid = process_named_running(bgp_daemon) # ensure we can also run this test on a live system - so lets clean # out the current configuration :) @@ -200,6 +201,9 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): cls.cli_set(cls, ['policy', 'prefix-list6', prefix_list_out6, 'rule', '10', 'action', 'deny']) cls.cli_set(cls, ['policy', 'prefix-list6', prefix_list_out6, 'rule', '10', 'prefix', '2001:db8:2000::/64']) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME + @classmethod def tearDownClass(cls): cls.cli_delete(cls, ['policy', 'route-map']) @@ -217,8 +221,11 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() + frrconfig = self.getFRRconfig('router bgp', endsection='^exit') + self.assertNotIn(f'router bgp', frrconfig) + # check process health and continuity - self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) + self.assertEqual(self.daemon_pid, process_named_running(bgp_daemon)) def create_bgp_instances_for_import_test(self): table = '1000' @@ -369,7 +376,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' bgp router-id {router_id}', frrconfig) self.assertIn(f' bgp allow-martian-nexthop', frrconfig) @@ -395,15 +402,21 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertNotIn(f'bgp ebgp-requires-policy', frrconfig) self.assertIn(f' no bgp suppress-duplicates', frrconfig) - afiv4_config = self.getFRRconfig(' address-family ipv4 unicast') + afiv4_config = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', + substring=' address-family ipv4 unicast', + endsubsection='^ exit-address-family') self.assertIn(f' maximum-paths {max_path_v4}', afiv4_config) self.assertIn(f' maximum-paths ibgp {max_path_v4ibgp}', afiv4_config) - afiv4_config = self.getFRRconfig(' address-family ipv4 labeled-unicast') + afiv4_config = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', + substring=' address-family ipv4 labeled-unicast', + endsubsection='^ exit-address-family') self.assertIn(f' maximum-paths {max_path_v4}', afiv4_config) self.assertIn(f' maximum-paths ibgp {max_path_v4ibgp}', afiv4_config) - afiv6_config = self.getFRRconfig(' address-family ipv6 unicast') + afiv6_config = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', + substring=' address-family ipv6 unicast', + endsubsection='^ exit-address-family') self.assertIn(f' maximum-paths {max_path_v6}', afiv6_config) self.assertIn(f' maximum-paths ibgp {max_path_v6ibgp}', afiv6_config) @@ -510,7 +523,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) for peer, peer_config in neighbor_config.items(): @@ -615,7 +628,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) for peer, peer_config in peer_group_config.items(): @@ -642,10 +655,71 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): } # We want to redistribute ... - redistributes = ['connected', 'isis', 'kernel', 'ospf', 'rip', 'static'] - for redistribute in redistributes: - self.cli_set(base_path + ['address-family', 'ipv4-unicast', - 'redistribute', redistribute]) + redistributes = { + 'babel' : { + 'metric' : '100', + 'route_map' : 'redistr-ipv4-babel', + }, + 'connected' : { + 'metric' : '200', + 'route_map' : 'redistr-ipv4-connected', + }, + 'isis' : { + 'metric' : '300', + 'route_map' : 'redistr-ipv4-isis', + }, + 'kernel' : { + 'metric' : '400', + 'route_map' : 'redistr-ipv4-kernel', + }, + 'nhrp': { + 'metric': '400', + 'route_map': 'redistr-ipv4-nhrp', + }, + 'ospf' : { + 'metric' : '500', + 'route_map' : 'redistr-ipv4-ospf', + }, + 'rip' : { + 'metric' : '600', + 'route_map' : 'redistr-ipv4-rip', + }, + 'static' : { + 'metric' : '700', + 'route_map' : 'redistr-ipv4-static', + }, + 'table' : { + '10' : { + 'metric' : '810', + 'route_map' : 'redistr-ipv4-table-10', + }, + '20' : { + 'metric' : '820', + 'route_map' : 'redistr-ipv4-table-20', + }, + '30' : { + 'metric' : '830', + 'route_map' : 'redistr-ipv4-table-30', + }, + }, + } + for proto, proto_config in redistributes.items(): + proto_path = base_path + ['address-family', 'ipv4-unicast', 'redistribute', proto] + if proto == 'table': + for table, table_config in proto_config.items(): + self.cli_set(proto_path + [table]) + if 'metric' in table_config: + self.cli_set(proto_path + [table, 'metric'], value=table_config['metric']) + if 'route_map' in table_config: + self.cli_set(['policy', 'route-map', table_config['route_map'], 'rule', '10', 'action'], value='permit') + self.cli_set(proto_path + [table, 'route-map'], value=table_config['route_map']) + else: + self.cli_set(proto_path) + if 'metric' in proto_config: + self.cli_set(proto_path + ['metric', proto_config['metric']]) + if 'route_map' in proto_config: + self.cli_set(['policy', 'route-map', proto_config['route_map'], 'rule', '10', 'action', 'permit']) + self.cli_set(proto_path + ['route-map', proto_config['route_map']]) for network, network_config in networks.items(): self.cli_set(base_path + ['address-family', 'ipv4-unicast', @@ -664,12 +738,31 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) - self.assertIn(f' address-family ipv4 unicast', frrconfig) - - for redistribute in redistributes: - self.assertIn(f' redistribute {redistribute}', frrconfig) + self.assertIn(' address-family ipv4 unicast', frrconfig) + + for proto, proto_config in redistributes.items(): + if proto == 'table': + for table, table_config in proto_config.items(): + tmp = f' redistribute table-direct {table}' + if 'metric' in proto_config: + metric = proto_config['metric'] + tmp += f' metric {metric}' + if 'route_map' in proto_config: + route_map = proto_config['route_map'] + tmp += f' route-map {route_map}' + self.assertIn(tmp, frrconfig) + else: + tmp = f' redistribute {proto}' + if 'metric' in proto_config: + metric = proto_config['metric'] + tmp += f' metric {metric}' + if 'route_map' in proto_config: + route_map = proto_config['route_map'] + tmp += f' route-map {route_map}' + + self.assertIn(tmp, frrconfig) for network, network_config in networks.items(): self.assertIn(f' network {network}', frrconfig) @@ -682,6 +775,10 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): command = f'{command} route-map {network_config["route_map"]}' self.assertIn(command, frrconfig) + for proto, proto_config in redistributes.items(): + if 'route_map' in proto_config: + self.cli_delete(['policy', 'route-map', proto_config['route_map']]) + def test_bgp_05_afi_ipv6(self): networks = { '2001:db8:100::/48' : { @@ -694,10 +791,67 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): } # We want to redistribute ... - redistributes = ['connected', 'kernel', 'ospfv3', 'ripng', 'static'] - for redistribute in redistributes: - self.cli_set(base_path + ['address-family', 'ipv6-unicast', - 'redistribute', redistribute]) + redistributes = { + 'babel' : { + 'metric' : '100', + 'route_map' : 'redistr-ipv6-babel', + }, + 'connected' : { + 'metric' : '200', + 'route_map' : 'redistr-ipv6-connected', + }, + 'isis' : { + 'metric' : '300', + 'route_map' : 'redistr-ipv6-isis', + }, + 'kernel' : { + 'metric' : '400', + 'route_map' : 'redistr-ipv6-kernel', + }, + 'ospfv3' : { + 'metric' : '500', + 'route_map' : 'redistr-ipv6-ospfv3', + }, + 'ripng' : { + 'metric' : '600', + 'route_map' : 'redistr-ipv6-ripng', + }, + 'static' : { + 'metric' : '700', + 'route_map' : 'redistr-ipv6-static', + }, + 'table' : { + '110' : { + 'metric' : '811', + 'route_map' : 'redistr-ipv6-table-110', + }, + '120' : { + 'metric' : '821', + 'route_map' : 'redistr-ipv6-table-120', + }, + '130' : { + 'metric' : '831', + 'route_map' : 'redistr-ipv6-table-130', + }, + }, + } + for proto, proto_config in redistributes.items(): + proto_path = base_path + ['address-family', 'ipv6-unicast', 'redistribute', proto] + if proto == 'table': + for table, table_config in proto_config.items(): + self.cli_set(proto_path + [table]) + if 'metric' in table_config: + self.cli_set(proto_path + [table, 'metric'], value=table_config['metric']) + if 'route_map' in table_config: + self.cli_set(['policy', 'route-map', table_config['route_map'], 'rule', '10', 'action'], value='permit') + self.cli_set(proto_path + [table, 'route-map'], value=table_config['route_map']) + else: + self.cli_set(proto_path) + if 'metric' in proto_config: + self.cli_set(proto_path + ['metric', proto_config['metric']]) + if 'route_map' in proto_config: + self.cli_set(['policy', 'route-map', proto_config['route_map'], 'rule', '20', 'action', 'permit']) + self.cli_set(proto_path + ['route-map', proto_config['route_map']]) for network, network_config in networks.items(): self.cli_set(base_path + ['address-family', 'ipv6-unicast', @@ -710,24 +864,47 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) - self.assertIn(f' address-family ipv6 unicast', frrconfig) + self.assertIn(' address-family ipv6 unicast', frrconfig) # T2100: By default ebgp-requires-policy is disabled to keep VyOS # 1.3 and 1.2 backwards compatibility - self.assertIn(f' no bgp ebgp-requires-policy', frrconfig) - - for redistribute in redistributes: - # FRR calls this OSPF6 - if redistribute == 'ospfv3': - redistribute = 'ospf6' - self.assertIn(f' redistribute {redistribute}', frrconfig) + self.assertIn(' no bgp ebgp-requires-policy', frrconfig) + + for proto, proto_config in redistributes.items(): + if proto == 'table': + for table, table_config in proto_config.items(): + tmp = f' redistribute table-direct {table}' + if 'metric' in proto_config: + metric = proto_config['metric'] + tmp += f' metric {metric}' + if 'route_map' in proto_config: + route_map = proto_config['route_map'] + tmp += f' route-map {route_map}' + self.assertIn(tmp, frrconfig) + else: + # FRR calls this OSPF6 + if proto == 'ospfv3': + proto = 'ospf6' + tmp = f' redistribute {proto}' + if 'metric' in proto_config: + metric = proto_config['metric'] + tmp += f' metric {metric}' + if 'route_map' in proto_config: + route_map = proto_config['route_map'] + tmp += f' route-map {route_map}' + + self.assertIn(tmp, frrconfig) for network, network_config in networks.items(): self.assertIn(f' network {network}', frrconfig) if 'as_set' in network_config: self.assertIn(f' aggregate-address {network} summary-only', frrconfig) + for proto, proto_config in redistributes.items(): + if 'route_map' in proto_config: + self.cli_delete(['policy', 'route-map', proto_config['route_map']]) + def test_bgp_06_listen_range(self): # Implemented via T1875 limit = '64' @@ -752,7 +929,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' neighbor {peer_group} peer-group', frrconfig) self.assertIn(f' neighbor {peer_group} remote-as {ASN}', frrconfig) @@ -787,7 +964,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' address-family l2vpn evpn', frrconfig) self.assertIn(f' advertise-all-vni', frrconfig) @@ -800,7 +977,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' flooding disable', frrconfig) self.assertIn(f' mac-vrf soo {soo}', frrconfig) for vni in vnis: - vniconfig = self.getFRRconfig(f' vni {vni}') + vniconfig = self.getFRRconfig(f' vni {vni}', endsection='^ exit-vni') self.assertIn(f'vni {vni}', vniconfig) self.assertIn(f' advertise-default-gw', vniconfig) self.assertIn(f' advertise-svi-ip', vniconfig) @@ -843,7 +1020,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR distances configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) for family in verify_families: self.assertIn(f'address-family {family}', frrconfig) @@ -881,7 +1058,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' address-family ipv6 unicast', frrconfig) @@ -889,7 +1066,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' import vrf {vrf}', frrconfig) # Verify FRR bgpd configuration - frr_vrf_config = self.getFRRconfig(f'router bgp {ASN} vrf {vrf}') + frr_vrf_config = self.getFRRconfig(f'router bgp {ASN} vrf {vrf}', endsection='^exit') self.assertIn(f'router bgp {ASN} vrf {vrf}', frr_vrf_config) self.assertIn(f' bgp router-id {router_id}', frr_vrf_config) @@ -907,7 +1084,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' bgp router-id {router_id}', frrconfig) self.assertIn(f' bgp confederation identifier {confed_id}', frrconfig) @@ -924,7 +1101,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' neighbor {interface} interface v6only remote-as {remote_asn}', frrconfig) self.assertIn(f' address-family ipv6 unicast', frrconfig) @@ -956,11 +1133,13 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) for afi in ['ipv4', 'ipv6']: - afi_config = self.getFRRconfig(f' address-family {afi} unicast', endsection='exit-address-family', daemon='bgpd') + afi_config = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', + substring=f' address-family {afi} unicast', + endsubsection='^ exit-address-family') self.assertIn(f'address-family {afi} unicast', afi_config) self.assertIn(f' export vpn', afi_config) self.assertIn(f' import vpn', afi_config) @@ -1005,7 +1184,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' neighbor {neighbor} peer-group {peer_group}', frrconfig) self.assertIn(f' neighbor {peer_group} peer-group', frrconfig) @@ -1030,7 +1209,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' neighbor {neighbor} remote-as {remote_asn}', frrconfig) self.assertIn(f' neighbor {neighbor} local-as {local_asn}', frrconfig) @@ -1055,8 +1234,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): base_path + ['address-family', import_afi, 'import', 'vrf', import_vrf]) self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}') - frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f'address-family ipv4 unicast', frrconfig) @@ -1078,8 +1257,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): base_path + ['address-family', import_afi, 'import', 'vrf', import_vrf]) self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}') - frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f'address-family ipv4 unicast', frrconfig) self.assertIn(f' import vrf {import_vrf}', frrconfig) @@ -1092,8 +1271,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): # Verify deleting existent vrf default if other vrfs were created self.create_bgp_instances_for_import_test() self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}') - frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f'router bgp {ASN} vrf {import_vrf}', frrconfig_vrf) self.cli_delete(base_path) @@ -1109,8 +1288,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): 'vpn', 'export', import_rd]) self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}') - frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f'router bgp {ASN} vrf {import_vrf}', frrconfig_vrf) self.assertIn(f'address-family ipv4 unicast', frrconfig_vrf) @@ -1139,7 +1318,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() for interface in interfaces: - frrconfig = self.getFRRconfig(f'interface {interface}') + frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', frrconfig) self.assertIn(f' mpls bgp forwarding', frrconfig) @@ -1153,7 +1332,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() for interface in interfaces: - frrconfig = self.getFRRconfig(f'interface {interface}') + frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', frrconfig) self.assertIn(f' mpls bgp forwarding', frrconfig) self.cli_delete(['interfaces', 'ethernet', interface, 'vrf']) @@ -1173,7 +1352,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path + ['address-family', 'ipv4-unicast', 'sid']) self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' segment-routing srv6', frrconfig) self.assertIn(f' locator {locator_name}', frrconfig) @@ -1188,17 +1367,22 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' segment-routing srv6', frrconfig) self.assertIn(f' locator {locator_name}', frrconfig) - afiv4_config = self.getFRRconfig(' address-family ipv4 unicast') + afiv4_config = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', + substring=' address-family ipv4 unicast', + endsubsection='^ exit-address-family') self.assertIn(f' sid vpn export {sid}', afiv4_config) self.assertIn(f' nexthop vpn export {nexthop_ipv4}', afiv4_config) - afiv6_config = self.getFRRconfig(' address-family ipv6 unicast') + + afiv6_config = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', + substring=' address-family ipv6 unicast', + endsubsection='^ exit-address-family') self.assertIn(f' sid vpn export {sid}', afiv6_config) - self.assertIn(f' nexthop vpn export {nexthop_ipv6}', afiv4_config) + self.assertIn(f' nexthop vpn export {nexthop_ipv6}', afiv6_config) def test_bgp_25_ipv4_labeled_unicast_peer_group(self): pg_ipv4 = 'foo4' @@ -1212,14 +1396,16 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' neighbor {pg_ipv4} peer-group', frrconfig) self.assertIn(f' neighbor {pg_ipv4} remote-as external', frrconfig) self.assertIn(f' bgp listen range {ipv4_prefix} peer-group {pg_ipv4}', frrconfig) self.assertIn(f' bgp labeled-unicast ipv4-explicit-null', frrconfig) - afiv4_config = self.getFRRconfig(' address-family ipv4 labeled-unicast') + afiv4_config = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', + substring=' address-family ipv4 labeled-unicast', + endsubsection='^ exit-address-family') self.assertIn(f' neighbor {pg_ipv4} activate', afiv4_config) self.assertIn(f' neighbor {pg_ipv4} maximum-prefix {ipv4_max_prefix}', afiv4_config) @@ -1236,14 +1422,16 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' neighbor {pg_ipv6} peer-group', frrconfig) self.assertIn(f' neighbor {pg_ipv6} remote-as external', frrconfig) self.assertIn(f' bgp listen range {ipv6_prefix} peer-group {pg_ipv6}', frrconfig) self.assertIn(f' bgp labeled-unicast ipv6-explicit-null', frrconfig) - afiv6_config = self.getFRRconfig(' address-family ipv6 labeled-unicast') + afiv6_config = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', + substring=' address-family ipv6 labeled-unicast', + endsubsection='^ exit-address-family') self.assertIn(f' neighbor {pg_ipv6} activate', afiv6_config) self.assertIn(f' neighbor {pg_ipv6} maximum-prefix {ipv6_max_prefix}', afiv6_config) @@ -1255,7 +1443,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['peer-group', 'peer1', 'remote-as', 'internal']) self.cli_commit() - conf = self.getFRRconfig(' address-family l2vpn evpn') + conf = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', + substring=' address-family l2vpn evpn', endsubsection='^ exit-address-family') self.assertIn('neighbor peer1 route-reflector-client', conf) @@ -1294,7 +1483,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['neighbor', int_neighbors[1], 'remote-as', ASN]) self.cli_commit() - conf = self.getFRRconfig(f'router bgp {ASN}') + conf = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') _common_config_check(conf) # test add internal remote-as to external group @@ -1309,7 +1498,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['neighbor', ext_neighbors[1], 'remote-as', f'{int(ASN) + 2}']) self.cli_commit() - conf = self.getFRRconfig(f'router bgp {ASN}') + conf = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') _common_config_check(conf) self.assertIn(f'neighbor {ext_neighbors[1]} remote-as {int(ASN) + 2}', conf) self.assertIn(f'neighbor {ext_neighbors[1]} peer-group {ext_pg_name}', conf) @@ -1321,7 +1510,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['neighbor', ext_neighbors[1], 'remote-as', 'external']) self.cli_commit() - conf = self.getFRRconfig(f'router bgp {ASN}') + conf = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') _common_config_check(conf, include_ras=False) self.assertIn(f'neighbor {int_neighbors[0]} remote-as internal', conf) @@ -1346,11 +1535,47 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() - conf = self.getFRRconfig(f'router bgp {ASN}') + conf = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'neighbor OVERLAY remote-as {int(ASN) + 1}', conf) self.assertIn(f'neighbor OVERLAY local-as {int(ASN) + 1}', conf) + def test_bgp_30_import_vrf_routemap(self): + router_id = '127.0.0.3' + table = '1000' + vrf = 'red' + vrf_base = ['vrf', 'name', vrf] + self.cli_set(vrf_base + ['table', table]) + self.cli_set(vrf_base + ['protocols', 'bgp', 'system-as', ASN]) + self.cli_set( + vrf_base + ['protocols', 'bgp', 'parameters', 'router-id', + router_id]) + + self.cli_set( + base_path + ['address-family', 'ipv4-unicast', 'import', + 'vrf', vrf]) + self.cli_set( + base_path + ['address-family', 'ipv4-unicast', 'route-map', + 'vrf', 'import', route_map_in]) + + self.cli_commit() + + # Verify FRR bgpd configuration + frrconfig = self.getFRRconfig(f'router bgp {ASN}', + endsection='^exit') + self.assertIn(f'router bgp {ASN}', frrconfig) + self.assertIn(f' address-family ipv4 unicast', frrconfig) + + self.assertIn(f' import vrf {vrf}', frrconfig) + self.assertIn(f' import vrf route-map {route_map_in}', frrconfig) + + # Verify FRR bgpd configuration + frr_vrf_config = self.getFRRconfig( + f'router bgp {ASN} vrf {vrf}', endsection='^exit') + self.assertIn(f'router bgp {ASN} vrf {vrf}', frr_vrf_config) + self.assertIn(f' bgp router-id {router_id}', frr_vrf_config) + + def test_bgp_99_bmp(self): target_name = 'instance-bmp' target_address = '127.0.0.1' @@ -1379,7 +1604,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): # let the bgpd process recover sleep(10) # update daemon PID - this was a planned daemon restart - self.daemon_pid = process_named_running(PROCESS_NAME) + self.daemon_pid = process_named_running(bgp_daemon) # set bmp config but not set address self.cli_set(target_path + ['port', target_port]) @@ -1399,7 +1624,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify bgpd bmp configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'bmp mirror buffer-limit {mirror_buffer}', frrconfig) self.assertIn(f'bmp targets {target_name}', frrconfig) self.assertIn(f'bmp mirror', frrconfig) diff --git a/smoketest/scripts/cli/test_protocols_isis.py b/smoketest/scripts/cli/test_protocols_isis.py index 769f3dd33..14e833fd9 100755 --- a/smoketest/scripts/cli/test_protocols_isis.py +++ b/smoketest/scripts/cli/test_protocols_isis.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright (C) 2021-2025 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 @@ -17,13 +17,14 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME + from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section from vyos.utils.process import process_named_running +from vyos.frrender import isis_daemon -PROCESS_NAME = 'isisd' base_path = ['protocols', 'isis'] - domain = 'VyOS' net = '49.0001.1921.6800.1002.00' @@ -34,11 +35,13 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): # call base-classes classmethod super(TestProtocolsISIS, cls).setUpClass() # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same - cls.daemon_pid = process_named_running(PROCESS_NAME) + cls.daemon_pid = process_named_running(isis_daemon) # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) cls.cli_delete(cls, ['vrf']) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME def tearDown(self): # cleanup any possible VRF mess @@ -49,19 +52,14 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # check process health and continuity - self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) - - def isis_base_config(self): - self.cli_set(base_path + ['net', net]) - for interface in self._interfaces: - self.cli_set(base_path + ['interface', interface]) + self.assertEqual(self.daemon_pid, process_named_running(isis_daemon)) def test_isis_01_redistribute(self): prefix_list = 'EXPORT-ISIS' route_map = 'EXPORT-ISIS' rule = '10' metric_style = 'transition' - + redistribute = ['babel', 'bgp', 'connected', 'kernel', 'nhrp', 'ospf', 'rip', 'static'] self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', rule, 'action', 'permit']) self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', rule, 'prefix', '203.0.113.0/24']) self.cli_set(['policy', 'route-map', route_map, 'rule', rule, 'action', 'permit']) @@ -73,14 +71,18 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): with self.assertRaises(ConfigSessionError): self.cli_commit() - self.isis_base_config() + self.cli_set(base_path + ['net', net]) + for interface in self._interfaces: + self.cli_set(base_path + ['interface', interface]) self.cli_set(base_path + ['redistribute', 'ipv4', 'connected']) # verify() - Redistribute level-1 or level-2 should be specified with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(base_path + ['redistribute', 'ipv4', 'connected', 'level-2', 'route-map', route_map]) + for proto in redistribute: + self.cli_set(base_path + ['redistribute', 'ipv4', proto, 'level-2', 'route-map', route_map]) + self.cli_set(base_path + ['metric-style', metric_style]) self.cli_set(base_path + ['log-adjacency-changes']) @@ -88,14 +90,15 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd') + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' metric-style {metric_style}', tmp) self.assertIn(f' log-adjacency-changes', tmp) - self.assertIn(f' redistribute ipv4 connected level-2 route-map {route_map}', tmp) + for proto in redistribute: + self.assertIn(f' redistribute ipv4 {proto} level-2 route-map {route_map}', tmp) for interface in self._interfaces: - tmp = self.getFRRconfig(f'interface {interface}', daemon='isisd') + tmp = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f' ip router isis {domain}', tmp) self.assertIn(f' ipv6 router isis {domain}', tmp) @@ -124,11 +127,11 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR isisd configuration - tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd') + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f'router isis {domain}', tmp) self.assertIn(f' net {net}', tmp) - tmp = self.getFRRconfig(f'router isis {domain} vrf {vrf}', daemon='isisd') + tmp = self.getFRRconfig(f'router isis {domain} vrf {vrf}', endsection='^exit') self.assertIn(f'router isis {domain} vrf {vrf}', tmp) self.assertIn(f' net {net}', tmp) self.assertIn(f' advertise-high-metrics', tmp) @@ -141,7 +144,10 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): metric = '50' route_map = 'default-foo-' - self.isis_base_config() + self.cli_set(base_path + ['net', net]) + for interface in self._interfaces: + self.cli_set(base_path + ['interface', interface]) + for afi in ['ipv4', 'ipv6']: for level in ['level-1', 'level-2']: self.cli_set(base_path + ['default-information', 'originate', afi, level, 'always']) @@ -152,7 +158,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd') + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) for afi in ['ipv4', 'ipv6']: @@ -160,11 +166,10 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): route_map_name = route_map + level + afi self.assertIn(f' default-information originate {afi} {level} always route-map {route_map_name} metric {metric}', tmp) - def test_isis_05_password(self): password = 'foo' - self.isis_base_config() + self.cli_set(base_path + ['net', net]) for interface in self._interfaces: self.cli_set(base_path + ['interface', interface, 'password', 'plaintext-password', f'{password}-{interface}']) @@ -187,13 +192,13 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd') + tmp = self.getFRRconfig(f'router isis {domain}', endsection='exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' domain-password clear {password}', tmp) self.assertIn(f' area-password clear {password}', tmp) for interface in self._interfaces: - tmp = self.getFRRconfig(f'interface {interface}', daemon='isisd') + tmp = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f' isis password clear {password}-{interface}', tmp) def test_isis_06_spf_delay_bfd(self): @@ -235,12 +240,12 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd') + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' spf-delay-ietf init-delay {init_delay} short-delay {short_delay} long-delay {long_delay} holddown {holddown} time-to-learn {time_to_learn}', tmp) for interface in self._interfaces: - tmp = self.getFRRconfig(f'interface {interface}', daemon='isisd') + tmp = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f' ip router isis {domain}', tmp) self.assertIn(f' ipv6 router isis {domain}', tmp) self.assertIn(f' isis network {network}', tmp) @@ -252,7 +257,6 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): global_block_high = "399" local_block_low = "400" local_block_high = "499" - interface = 'lo' maximum_stack_size = '5' prefix_one = '192.168.0.1/32' prefix_two = '192.168.0.2/32' @@ -264,7 +268,9 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): prefix_four_value = '65000' self.cli_set(base_path + ['net', net]) - self.cli_set(base_path + ['interface', interface]) + for interface in self._interfaces: + self.cli_set(base_path + ['interface', interface]) + self.cli_set(base_path + ['segment-routing', 'maximum-label-depth', maximum_stack_size]) self.cli_set(base_path + ['segment-routing', 'global-block', 'low-label-value', global_block_low]) self.cli_set(base_path + ['segment-routing', 'global-block', 'high-label-value', global_block_high]) @@ -283,7 +289,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd') + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' segment-routing on', tmp) self.assertIn(f' segment-routing global-block {global_block_low} {global_block_high} local-block {local_block_low} {local_block_high}', tmp) @@ -305,7 +311,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify main ISIS changes - tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd') + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' mpls ldp-sync', tmp) self.assertIn(f' mpls ldp-sync holddown {holddown}', tmp) @@ -318,7 +324,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): for interface in self._interfaces: # Verify interface changes for holddown - tmp = self.getFRRconfig(f'interface {interface}', daemon='isisd') + tmp = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', tmp) self.assertIn(f' ip router isis {domain}', tmp) self.assertIn(f' ipv6 router isis {domain}', tmp) @@ -332,7 +338,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): for interface in self._interfaces: # Verify interface changes for disable - tmp = self.getFRRconfig(f'interface {interface}', daemon='isisd') + tmp = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', tmp) self.assertIn(f' ip router isis {domain}', tmp) self.assertIn(f' ipv6 router isis {domain}', tmp) @@ -355,7 +361,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): for level in ['level-1', 'level-2']: self.cli_set(base_path + ['fast-reroute', 'lfa', 'remote', 'prefix-list', prefix_list, level]) self.cli_commit() - tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd') + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' fast-reroute remote-lfa prefix-list {prefix_list} {level}', tmp) self.cli_delete(base_path + ['fast-reroute']) @@ -365,7 +371,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): for level in ['level-1', 'level-2']: self.cli_set(base_path + ['fast-reroute', 'lfa', 'local', 'load-sharing', 'disable', level]) self.cli_commit() - tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd') + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' fast-reroute load-sharing disable {level}', tmp) self.cli_delete(base_path + ['fast-reroute']) @@ -376,7 +382,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): for level in ['level-1', 'level-2']: self.cli_set(base_path + ['fast-reroute', 'lfa', 'local', 'priority-limit', priority, level]) self.cli_commit() - tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd') + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' fast-reroute priority-limit {priority} {level}', tmp) self.cli_delete(base_path + ['fast-reroute']) @@ -388,7 +394,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): for level in ['level-1', 'level-2']: self.cli_set(base_path + ['fast-reroute', 'lfa', 'local', 'tiebreaker', tiebreaker, 'index', index, level]) self.cli_commit() - tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd') + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' fast-reroute lfa tiebreaker {tiebreaker} index {index} {level}', tmp) self.cli_delete(base_path + ['fast-reroute']) @@ -408,7 +414,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): for topology in topologies: self.cli_set(base_path + ['topology', topology]) self.cli_commit() - tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd') + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' topology {topology}', tmp) diff --git a/smoketest/scripts/cli/test_protocols_mpls.py b/smoketest/scripts/cli/test_protocols_mpls.py index 0c1599f9b..3840c24f4 100755 --- a/smoketest/scripts/cli/test_protocols_mpls.py +++ b/smoketest/scripts/cli/test_protocols_mpls.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2025 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 @@ -17,11 +17,13 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME + from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section +from vyos.frrender import ldpd_daemon from vyos.utils.process import process_named_running -PROCESS_NAME = 'ldpd' base_path = ['protocols', 'mpls', 'ldp'] peers = { @@ -71,18 +73,19 @@ class TestProtocolsMPLS(VyOSUnitTestSHIM.TestCase): super(TestProtocolsMPLS, cls).setUpClass() # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same - cls.daemon_pid = process_named_running(PROCESS_NAME) - + cls.daemon_pid = process_named_running(ldpd_daemon) # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME def tearDown(self): self.cli_delete(base_path) self.cli_commit() # check process health and continuity - self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) + self.assertEqual(self.daemon_pid, process_named_running(ldpd_daemon)) def test_mpls_basic(self): router_id = '1.2.3.4' @@ -106,15 +109,86 @@ class TestProtocolsMPLS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Validate configuration - frrconfig = self.getFRRconfig('mpls ldp', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('mpls ldp', endsection='^exit') + self.assertIn(f'mpls ldp', frrconfig) + self.assertIn(f' router-id {router_id}', frrconfig) + + # Validate AFI IPv4 + afiv4_config = self.getFRRconfig('mpls ldp', endsection='^exit', + substring=' address-family ipv4', + endsubsection='^ exit-address-family') + self.assertIn(f' discovery transport-address {transport_ipv4_addr}', afiv4_config) + for interface in interfaces: + self.assertIn(f' interface {interface}', afiv4_config) + + def test_02_mpls_disable_establish_hello(self): + router_id = '1.2.3.4' + transport_ipv4_addr = '5.6.7.8' + transport_ipv6_addr = '2001:db8:1111::1111' + interfaces = Section.interfaces('ethernet') + + self.cli_set(base_path + ['router-id', router_id]) + + # At least one LDP interface must be configured + with self.assertRaises(ConfigSessionError): + self.cli_commit() + for interface in interfaces: + self.cli_set(base_path + ['interface', interface, 'disable-establish-hello']) + + # LDP transport address missing + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + ['discovery', 'transport-ipv4-address', transport_ipv4_addr]) + self.cli_set(base_path + ['discovery', 'transport-ipv6-address', transport_ipv6_addr]) + + # Commit changes + self.cli_commit() + + # Validate configuration + frrconfig = self.getFRRconfig('mpls ldp', endsection='^exit') self.assertIn(f'mpls ldp', frrconfig) self.assertIn(f' router-id {router_id}', frrconfig) # Validate AFI IPv4 - afiv4_config = self.getFRRconfig(' address-family ipv4', daemon=PROCESS_NAME) + afiv4_config = self.getFRRconfig('mpls ldp', endsection='^exit', + substring=' address-family ipv4', + endsubsection='^ exit-address-family') self.assertIn(f' discovery transport-address {transport_ipv4_addr}', afiv4_config) for interface in interfaces: self.assertIn(f' interface {interface}', afiv4_config) + self.assertIn(f' disable-establish-hello', afiv4_config) + + # Validate AFI IPv6 + afiv6_config = self.getFRRconfig('mpls ldp', endsection='^exit', + substring=' address-family ipv6', + endsubsection='^ exit-address-family') + self.assertIn(f' discovery transport-address {transport_ipv6_addr}', afiv6_config) + for interface in interfaces: + self.assertIn(f' interface {interface}', afiv6_config) + self.assertIn(f' disable-establish-hello', afiv6_config) + + # Delete disable-establish-hello + for interface in interfaces: + self.cli_delete(base_path + ['interface', interface, 'disable-establish-hello']) + + # Commit changes + self.cli_commit() + + # Validate AFI IPv4 + afiv4_config = self.getFRRconfig('mpls ldp', endsection='^exit', + substring=' address-family ipv4', + endsubsection='^ exit-address-family') + # Validate AFI IPv6 + afiv6_config = self.getFRRconfig('mpls ldp', endsection='^exit', + substring=' address-family ipv6', + endsubsection='^ exit-address-family') + # Check deleted 'disable-establish-hello' option per interface + for interface in interfaces: + self.assertIn(f' interface {interface}', afiv4_config) + self.assertNotIn(f' disable-establish-hello', afiv4_config) + self.assertIn(f' interface {interface}', afiv6_config) + self.assertNotIn(f' disable-establish-hello', afiv6_config) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_nhrp.py b/smoketest/scripts/cli/test_protocols_nhrp.py index 43ae4abf2..73a760945 100755 --- a/smoketest/scripts/cli/test_protocols_nhrp.py +++ b/smoketest/scripts/cli/test_protocols_nhrp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright (C) 2021-2025 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 @@ -17,14 +17,12 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM - -from vyos.firewall import find_nftables_rule from vyos.utils.process import process_named_running -from vyos.utils.file import read_file tunnel_path = ['interfaces', 'tunnel'] nhrp_path = ['protocols', 'nhrp'] vpn_path = ['vpn', 'ipsec'] +PROCESS_NAME = 'nhrpd' class TestProtocolsNHRP(VyOSUnitTestSHIM.TestCase): @classmethod @@ -41,29 +39,41 @@ class TestProtocolsNHRP(VyOSUnitTestSHIM.TestCase): self.cli_delete(tunnel_path) self.cli_commit() - def test_config(self): + def test_01_nhrp_config(self): tunnel_if = "tun100" - tunnel_source = "192.0.2.1" + tunnel_ip = '172.16.253.134/32' + tunnel_source = "192.0.2.134" tunnel_encapsulation = "gre" esp_group = "ESP-HUB" ike_group = "IKE-HUB" nhrp_secret = "vyos123" nhrp_profile = "NHRPVPN" + nhrp_holdtime = '300' + nhs_tunnelip = '172.16.253.1' + nhs_nbmaip = '192.0.2.1' + map_tunnelip = '172.16.253.135' + map_nbmaip = "192.0.2.135" + nhrp_networkid = '1' ipsec_secret = "secret" - + multicat_log_group = '2' + redirect_log_group = '1' # Tunnel - self.cli_set(tunnel_path + [tunnel_if, "address", "172.16.253.134/29"]) + self.cli_set(tunnel_path + [tunnel_if, "address", tunnel_ip]) self.cli_set(tunnel_path + [tunnel_if, "encapsulation", tunnel_encapsulation]) self.cli_set(tunnel_path + [tunnel_if, "source-address", tunnel_source]) self.cli_set(tunnel_path + [tunnel_if, "enable-multicast"]) self.cli_set(tunnel_path + [tunnel_if, "parameters", "ip", "key", "1"]) # NHRP - self.cli_set(nhrp_path + ["tunnel", tunnel_if, "cisco-authentication", nhrp_secret]) - self.cli_set(nhrp_path + ["tunnel", tunnel_if, "holding-time", "300"]) - self.cli_set(nhrp_path + ["tunnel", tunnel_if, "multicast", "dynamic"]) + self.cli_set(nhrp_path + ["tunnel", tunnel_if, "authentication", nhrp_secret]) + self.cli_set(nhrp_path + ["tunnel", tunnel_if, "holdtime", nhrp_holdtime]) + self.cli_set(nhrp_path + ["tunnel", tunnel_if, "multicast", nhs_tunnelip]) self.cli_set(nhrp_path + ["tunnel", tunnel_if, "redirect"]) self.cli_set(nhrp_path + ["tunnel", tunnel_if, "shortcut"]) + self.cli_set(nhrp_path + ["tunnel", tunnel_if, "registration-no-unique"]) + self.cli_set(nhrp_path + ["tunnel", tunnel_if, "network-id", nhrp_networkid]) + self.cli_set(nhrp_path + ["tunnel", tunnel_if, "nhs", "tunnel-ip", nhs_tunnelip, "nbma", nhs_nbmaip]) + self.cli_set(nhrp_path + ["tunnel", tunnel_if, "map", "tunnel-ip", map_tunnelip, "nbma", map_nbmaip]) # IKE/ESP Groups self.cli_set(vpn_path + ["esp-group", esp_group, "lifetime", "1800"]) @@ -93,29 +103,40 @@ class TestProtocolsNHRP(VyOSUnitTestSHIM.TestCase): self.cli_commit() - opennhrp_lines = [ - f'interface {tunnel_if} #hub {nhrp_profile}', - f'cisco-authentication {nhrp_secret}', - f'holding-time 300', - f'shortcut', - f'multicast dynamic', - f'redirect' + frrconfig = self.getFRRconfig(f'interface {tunnel_if}', endsection='^exit') + self.assertIn(f'interface {tunnel_if}', frrconfig) + self.assertIn(f' ip nhrp authentication {nhrp_secret}', frrconfig) + self.assertIn(f' ip nhrp holdtime {nhrp_holdtime}', frrconfig) + self.assertIn(f' ip nhrp map multicast {nhs_tunnelip}', frrconfig) + self.assertIn(f' ip nhrp redirect', frrconfig) + self.assertIn(f' ip nhrp registration no-unique', frrconfig) + self.assertIn(f' ip nhrp shortcut', frrconfig) + self.assertIn(f' ip nhrp network-id {nhrp_networkid}', frrconfig) + self.assertIn(f' ip nhrp nhs {nhs_tunnelip} nbma {nhs_nbmaip}', frrconfig) + self.assertIn(f' ip nhrp map {map_tunnelip} {map_nbmaip}', frrconfig) + self.assertIn(f' tunnel protection vici profile dmvpn-{nhrp_profile}-{tunnel_if}-child', + frrconfig) + + nftables_search_multicast = [ + ['chain VYOS_NHRP_MULTICAST_OUTPUT'], + ['type filter hook output priority filter + 10; policy accept;'], + [f'oifname "{tunnel_if}"', 'ip daddr 224.0.0.0/24', 'counter', f'log group {multicat_log_group}'], + [f'oifname "{tunnel_if}"', 'ip daddr 224.0.0.0/24', 'counter', 'drop'], + ['chain VYOS_NHRP_MULTICAST_FORWARD'], + ['type filter hook output priority filter + 10; policy accept;'], + [f'oifname "{tunnel_if}"', 'ip daddr 224.0.0.0/4', 'counter', f'log group {multicat_log_group}'], + [f'oifname "{tunnel_if}"', 'ip daddr 224.0.0.0/4', 'counter', 'drop'] ] - tmp_opennhrp_conf = read_file('/run/opennhrp/opennhrp.conf') - - for line in opennhrp_lines: - self.assertIn(line, tmp_opennhrp_conf) - - firewall_matches = [ - f'ip protocol {tunnel_encapsulation}', - f'ip saddr {tunnel_source}', - f'ip daddr 224.0.0.0/4', - f'comment "VYOS_NHRP_{tunnel_if}"' + nftables_search_redirect = [ + ['chain VYOS_NHRP_REDIRECT_FORWARD'], + ['type filter hook forward priority filter + 10; policy accept;'], + [f'iifname "{tunnel_if}" oifname "{tunnel_if}"', 'meter loglimit-0 size 65535 { ip daddr & 255.255.255.0 . ip saddr & 255.255.255.0 timeout 1m limit rate 4/minute burst 1 packets }', 'counter', f'log group {redirect_log_group}'] ] + self.verify_nftables(nftables_search_multicast, 'ip vyos_nhrp_multicast') + self.verify_nftables(nftables_search_redirect, 'ip vyos_nhrp_redirect') - self.assertTrue(find_nftables_rule('ip vyos_nhrp_filter', 'VYOS_NHRP_OUTPUT', firewall_matches) is not None) - self.assertTrue(process_named_running('opennhrp')) + self.assertTrue(process_named_running(PROCESS_NAME)) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_openfabric.py b/smoketest/scripts/cli/test_protocols_openfabric.py index e37aed456..323b6cd74 100644 --- a/smoketest/scripts/cli/test_protocols_openfabric.py +++ b/smoketest/scripts/cli/test_protocols_openfabric.py @@ -17,10 +17,12 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME + from vyos.configsession import ConfigSessionError from vyos.utils.process import process_named_running +from vyos.frrender import openfabric_daemon -PROCESS_NAME = 'fabricd' base_path = ['protocols', 'openfabric'] domain = 'VyOS' @@ -36,17 +38,19 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase): # call base-classes classmethod super(TestProtocolsOpenFabric, cls).setUpClass() # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same - cls.daemon_pid = process_named_running(PROCESS_NAME) + cls.daemon_pid = process_named_running(openfabric_daemon) # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME def tearDown(self): self.cli_delete(base_path) self.cli_commit() # check process health and continuity - self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) + self.assertEqual(self.daemon_pid, process_named_running(openfabric_daemon)) def openfabric_base_config(self): self.cli_set(['interfaces', 'dummy', dummy_if]) @@ -75,14 +79,14 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - tmp = self.getFRRconfig(f'router openfabric {domain}', daemon='fabricd') + tmp = self.getFRRconfig(f'router openfabric {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' log-adjacency-changes', tmp) self.assertIn(f' set-overload-bit', tmp) self.assertIn(f' fabric-tier {fabric_tier}', tmp) self.assertIn(f' lsp-gen-interval {lsp_gen_interval}', tmp) - tmp = self.getFRRconfig(f'interface {dummy_if}', daemon='fabricd') + tmp = self.getFRRconfig(f'interface {dummy_if}', endsection='^exit') self.assertIn(f' ip router openfabric {domain}', tmp) self.assertIn(f' ipv6 router openfabric {domain}', tmp) @@ -101,12 +105,12 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR openfabric configuration - tmp = self.getFRRconfig(f'router openfabric {domain}', daemon='fabricd') + tmp = self.getFRRconfig(f'router openfabric {domain}', endsection='^exit') self.assertIn(f'router openfabric {domain}', tmp) self.assertIn(f' net {net}', tmp) # Verify interface configuration - tmp = self.getFRRconfig(f'interface {interface}', daemon='fabricd') + tmp = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f' ip router openfabric {domain}', tmp) # for lo interface 'openfabric passive' is implied self.assertIn(f' openfabric passive', tmp) @@ -137,11 +141,11 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - tmp = self.getFRRconfig(f'router openfabric {domain}', daemon='fabricd') + tmp = self.getFRRconfig(f'router openfabric {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' domain-password clear {password}', tmp) - tmp = self.getFRRconfig(f'interface {dummy_if}', daemon='fabricd') + tmp = self.getFRRconfig(f'interface {dummy_if}', endsection='^exit') self.assertIn(f' openfabric password clear {password}-{dummy_if}', tmp) def test_openfabric_multiple_domains(self): @@ -165,22 +169,21 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR openfabric configuration - tmp = self.getFRRconfig(f'router openfabric {domain}', daemon='fabricd') + tmp = self.getFRRconfig(f'router openfabric {domain}', endsection='^exit') self.assertIn(f'router openfabric {domain}', tmp) self.assertIn(f' net {net}', tmp) - tmp = self.getFRRconfig(f'router openfabric {domain_2}', daemon='fabricd') + tmp = self.getFRRconfig(f'router openfabric {domain_2}', endsection='^exit') self.assertIn(f'router openfabric {domain_2}', tmp) self.assertIn(f' net {net}', tmp) # Verify interface configuration - tmp = self.getFRRconfig(f'interface {dummy_if}', daemon='fabricd') + tmp = self.getFRRconfig(f'interface {dummy_if}', endsection='^exit') self.assertIn(f' ip router openfabric {domain}', tmp) self.assertIn(f' ipv6 router openfabric {domain}', tmp) - tmp = self.getFRRconfig(f'interface {interface}', daemon='fabricd') + tmp = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f' ip router openfabric {domain_2}', tmp) - if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_ospf.py b/smoketest/scripts/cli/test_protocols_ospf.py index 905eaf2e9..ea55fa031 100755 --- a/smoketest/scripts/cli/test_protocols_ospf.py +++ b/smoketest/scripts/cli/test_protocols_ospf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright (C) 2021-2025 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 @@ -16,13 +16,16 @@ import unittest +from time import sleep from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section +from vyos.frrender import ospf_daemon from vyos.utils.process import process_named_running +from vyos.xml_ref import default_value -PROCESS_NAME = 'ospfd' base_path = ['protocols', 'ospf'] route_map = 'foo-bar-baz10' @@ -34,7 +37,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): super(TestProtocolsOSPF, cls).setUpClass() # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same - cls.daemon_pid = process_named_running(PROCESS_NAME) + cls.daemon_pid = process_named_running(ospf_daemon) cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit']) cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '20', 'action', 'permit']) @@ -43,6 +46,8 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME @classmethod def tearDownClass(cls): @@ -54,8 +59,11 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') + self.assertNotIn(f'router ospf', frrconfig) + # check process health and continuity - self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) + self.assertEqual(self.daemon_pid, process_named_running(ospf_daemon)) def test_ospf_01_defaults(self): # commit changes @@ -63,7 +71,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' auto-cost reference-bandwidth 100', frrconfig) self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults @@ -91,7 +99,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' compatible rfc1583', frrconfig) self.assertIn(f' auto-cost reference-bandwidth {bandwidth}', frrconfig) @@ -107,7 +115,6 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.assertNotIn(f' area 10 range 10.0.1.0/24 not-advertise', frrconfig) self.assertIn(f' area 10 range 10.0.2.0/24 not-advertise', frrconfig) - def test_ospf_03_access_list(self): acl = '100' seq = '10' @@ -123,14 +130,13 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults for ptotocol in protocols: self.assertIn(f' distribute-list {acl} out {ptotocol}', frrconfig) # defaults self.cli_delete(['policy', 'access-list', acl]) - def test_ospf_04_default_originate(self): seq = '100' metric = '50' @@ -144,7 +150,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults self.assertIn(f' default-information originate metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig) @@ -154,10 +160,9 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f' default-information originate always metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig) - def test_ospf_05_options(self): global_distance = '128' intra_area = '100' @@ -196,7 +201,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' mpls-te on', frrconfig) self.assertIn(f' mpls-te router-address 0.0.0.0', frrconfig) # default @@ -219,9 +224,16 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['distance', 'ospf', 'inter-area', inter_area]) self.cli_commit() - frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f' distance ospf intra-area {intra_area} inter-area {inter_area} external {external}', frrconfig) + # https://github.com/FRRouting/frr/issues/17011 + # We need to wait on_shutdown time, until the OSPF process is removed from the CLI + # otherwise the test in tearDown() will fail + self.cli_delete(base_path) + self.cli_commit() + + sleep(int(on_shutdown) + 5) # additional grace period of 5 seconds def test_ospf_06_neighbor(self): priority = '10' @@ -235,7 +247,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) for neighbor in neighbors: self.assertIn(f' neighbor {neighbor} priority {priority} poll-interval {poll_interval}', frrconfig) # default @@ -243,7 +255,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): def test_ospf_07_redistribute(self): metric = '15' metric_type = '1' - redistribute = ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static'] + redistribute = ['babel', 'bgp', 'connected', 'isis', 'kernel', 'nhrp', 'rip', 'static'] for protocol in redistribute: self.cli_set(base_path + ['redistribute', protocol, 'metric', metric]) @@ -254,7 +266,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) for protocol in redistribute: self.assertIn(f' redistribute {protocol} metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig) @@ -268,6 +280,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): retransmit = '5' transmit = '5' dead = '40' + window_default = default_value(base_path + ['area', area, 'virtual-link', virtual_link, 'retransmit-window']) self.cli_set(base_path + ['area', area, 'shortcut', shortcut]) self.cli_set(base_path + ['area', area, 'virtual-link', virtual_link, 'hello-interval', hello]) @@ -281,10 +294,10 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' area {area} shortcut {shortcut}', frrconfig) - self.assertIn(f' area {area} virtual-link {virtual_link} hello-interval {hello} retransmit-interval {retransmit} transmit-delay {transmit} dead-interval {dead}', frrconfig) + self.assertIn(f' area {area} virtual-link {virtual_link} hello-interval {hello} retransmit-interval {retransmit} retransmit-window {window_default} transmit-delay {transmit} dead-interval {dead}', frrconfig) for network in networks: self.assertIn(f' network {network} area {area}', frrconfig) @@ -313,13 +326,13 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): # commit changes self.cli_commit() - frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' passive-interface default', frrconfig) for interface in interfaces: # Can not use daemon for getFRRconfig() as bandwidth parameter belongs to zebra process - config = self.getFRRconfig(f'interface {interface}') + config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', config) self.assertIn(f' ip ospf authentication-key {password}', config) self.assertIn(f' ip ospf bfd', config) @@ -337,7 +350,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): for interface in interfaces: # T5467: It must also be removed from FRR config - frrconfig = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertNotIn(f'interface {interface}', frrconfig) # There should be no OSPF related command at all under the interface self.assertNotIn(f' ip ospf', frrconfig) @@ -358,11 +371,11 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) for interface in interfaces: - config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', config) self.assertIn(f' ip ospf area {area}', config) @@ -385,17 +398,17 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' auto-cost reference-bandwidth 100', frrconfig) self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults - frrconfig = self.getFRRconfig(f'router ospf vrf {vrf}', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig(f'router ospf vrf {vrf}', endsection='^exit') self.assertIn(f'router ospf vrf {vrf}', frrconfig) self.assertIn(f' auto-cost reference-bandwidth 100', frrconfig) self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults - frrconfig = self.getFRRconfig(f'interface {vrf_iface}', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig(f'interface {vrf_iface}', endsection='^exit') self.assertIn(f'interface {vrf_iface}', frrconfig) self.assertIn(f' ip ospf area {area}', frrconfig) @@ -405,7 +418,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # T5467: It must also be removed from FRR config - frrconfig = self.getFRRconfig(f'interface {vrf_iface}', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig(f'interface {vrf_iface}', endsection='^exit') self.assertNotIn(f'interface {vrf_iface}', frrconfig) # There should be no OSPF related command at all under the interface self.assertNotIn(f' ip ospf', frrconfig) @@ -431,7 +444,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # default self.assertIn(f' network {network} area {area}', frrconfig) @@ -464,7 +477,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f' segment-routing on', frrconfig) self.assertIn(f' segment-routing global-block {global_block_low} {global_block_high} local-block {local_block_low} {local_block_high}', frrconfig) self.assertIn(f' segment-routing node-msd {maximum_stack_size}', frrconfig) @@ -482,7 +495,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify main OSPF changes - frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) self.assertIn(f' mpls ldp-sync holddown {holddown}', frrconfig) @@ -495,7 +508,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): for interface in interfaces: # Verify interface changes for holddown - config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', config) self.assertIn(f' ip ospf dead-interval 40', config) self.assertIn(f' ip ospf mpls ldp-sync', config) @@ -509,7 +522,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): for interface in interfaces: # Verify interface changes for disable - config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', config) self.assertIn(f' ip ospf dead-interval 40', config) self.assertNotIn(f' ip ospf mpls ldp-sync', config) @@ -531,7 +544,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' capability opaque', frrconfig) self.assertIn(f' graceful-restart grace-period {period}', frrconfig) @@ -557,7 +570,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit', empty_retry=60) self.assertIn(f'router ospf', frrconfig) self.assertIn(f' network {network} area {area1}', frrconfig) diff --git a/smoketest/scripts/cli/test_protocols_ospfv3.py b/smoketest/scripts/cli/test_protocols_ospfv3.py index 989e1552d..5da4c7c98 100755 --- a/smoketest/scripts/cli/test_protocols_ospfv3.py +++ b/smoketest/scripts/cli/test_protocols_ospfv3.py @@ -17,12 +17,13 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section +from vyos.frrender import ospf6_daemon from vyos.utils.process import process_named_running -PROCESS_NAME = 'ospf6d' base_path = ['protocols', 'ospfv3'] route_map = 'foo-bar-baz-0815' @@ -36,7 +37,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): super(TestProtocolsOSPFv3, cls).setUpClass() # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same - cls.daemon_pid = process_named_running(PROCESS_NAME) + cls.daemon_pid = process_named_running(ospf6_daemon) cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit']) cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '20', 'action', 'permit']) @@ -44,6 +45,8 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME @classmethod def tearDownClass(cls): @@ -54,8 +57,11 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') + self.assertNotIn(f'router ospf6', frrconfig) + # check process health and continuity - self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) + self.assertEqual(self.daemon_pid, process_named_running(ospf6_daemon)) def test_ospfv3_01_basic(self): seq = '10' @@ -78,7 +84,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') self.assertIn(f'router ospf6', frrconfig) self.assertIn(f' area {default_area} range {prefix}', frrconfig) self.assertIn(f' ospf6 router-id {router_id}', frrconfig) @@ -86,7 +92,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.assertIn(f' area {default_area} export-list {acl_name}', frrconfig) for interface in interfaces: - if_config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + if_config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'ipv6 ospf6 area {default_area}', if_config) self.cli_delete(['policy', 'access-list6', acl_name]) @@ -107,7 +113,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') self.assertIn(f'router ospf6', frrconfig) self.assertIn(f' distance {dist_global}', frrconfig) self.assertIn(f' distance ospf6 intra-area {dist_intra_area} inter-area {dist_inter_area} external {dist_external}', frrconfig) @@ -131,7 +137,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') self.assertIn(f'router ospf6', frrconfig) for protocol in redistribute: self.assertIn(f' redistribute {protocol} metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig) @@ -162,13 +168,13 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') self.assertIn(f'router ospf6', frrconfig) cost = '100' priority = '10' for interface in interfaces: - if_config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + if_config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', if_config) self.assertIn(f' ipv6 ospf6 bfd', if_config) self.assertIn(f' ipv6 ospf6 bfd profile {bfd_profile}', if_config) @@ -185,7 +191,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() for interface in interfaces: - if_config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + if_config = self.getFRRconfig(f'interface {interface}', endsection='^exit') # There should be no OSPF6 configuration at all after interface removal self.assertNotIn(f' ipv6 ospf6', if_config) @@ -201,7 +207,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') self.assertIn(f'router ospf6', frrconfig) self.assertIn(f' area {area_stub} stub', frrconfig) self.assertIn(f' area {area_stub_nosum} stub no-summary', frrconfig) @@ -227,7 +233,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') self.assertIn(f'router ospf6', frrconfig) self.assertIn(f' area {area_nssa} nssa', frrconfig) self.assertIn(f' area {area_nssa_nosum} nssa default-information-originate no-summary', frrconfig) @@ -247,7 +253,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') self.assertIn(f'router ospf6', frrconfig) self.assertIn(f' default-information originate metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig) @@ -256,7 +262,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') self.assertIn(f' default-information originate always metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig) @@ -282,15 +288,15 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') self.assertIn(f'router ospf6', frrconfig) self.assertIn(f' ospf6 router-id {router_id}', frrconfig) - frrconfig = self.getFRRconfig(f'interface {vrf_iface}', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig(f'interface {vrf_iface}', endsection='^exit') self.assertIn(f'interface {vrf_iface}', frrconfig) self.assertIn(f' ipv6 ospf6 bfd', frrconfig) - frrconfig = self.getFRRconfig(f'router ospf6 vrf {vrf}', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig(f'router ospf6 vrf {vrf}', endsection='^exit') self.assertIn(f'router ospf6 vrf {vrf}', frrconfig) self.assertIn(f' ospf6 router-id {router_id_vrf}', frrconfig) @@ -300,7 +306,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # T5467: It must also be removed from FRR config - frrconfig = self.getFRRconfig(f'interface {vrf_iface}', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig(f'interface {vrf_iface}', endsection='^exit') self.assertNotIn(f'interface {vrf_iface}', frrconfig) # There should be no OSPF related command at all under the interface self.assertNotIn(f' ipv6 ospf6', frrconfig) @@ -326,7 +332,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') self.assertIn(f'router ospf6', frrconfig) self.assertIn(f' graceful-restart grace-period {period}', frrconfig) self.assertIn(f' graceful-restart helper planned-only', frrconfig) diff --git a/smoketest/scripts/cli/test_protocols_pim.py b/smoketest/scripts/cli/test_protocols_pim.py index ccfced138..cc62769b3 100755 --- a/smoketest/scripts/cli/test_protocols_pim.py +++ b/smoketest/scripts/cli/test_protocols_pim.py @@ -17,24 +17,35 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.configsession import ConfigSessionError +from vyos.frrender import pim_daemon from vyos.ifconfig import Section from vyos.utils.process import process_named_running -PROCESS_NAME = 'pimd' base_path = ['protocols', 'pim'] class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + # call base-classes classmethod + super(TestProtocolsPIM, cls).setUpClass() + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME + def tearDown(self): # pimd process must be running - self.assertTrue(process_named_running(PROCESS_NAME)) + self.assertTrue(process_named_running(pim_daemon)) self.cli_delete(base_path) self.cli_commit() # pimd process must be stopped by now - self.assertFalse(process_named_running(PROCESS_NAME)) + self.assertFalse(process_named_running(pim_daemon)) def test_01_pim_basic(self): rp = '127.0.0.1' @@ -57,11 +68,11 @@ class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR pimd configuration - frrconfig = self.getFRRconfig(daemon=PROCESS_NAME) - self.assertIn(f'ip pim rp {rp} {group}', frrconfig) + frrconfig = self.getFRRconfig('router pim', endsection='^exit') + self.assertIn(f' rp {rp} {group}', frrconfig) for interface in interfaces: - frrconfig = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', frrconfig) self.assertIn(f' ip pim', frrconfig) self.assertIn(f' ip pim bfd', frrconfig) @@ -108,18 +119,18 @@ class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase): 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) + frrconfig = self.getFRRconfig('router pim', endsection='^exit') + self.assertIn(f' no send-v6-secondary', frrconfig) + self.assertIn(f' rp {rp} {group}', frrconfig) + self.assertIn(f' register-suppress-time {register_suppress_time}', frrconfig) + self.assertIn(f' join-prune-interval {join_prune_interval}', frrconfig) + self.assertIn(f' packets {packets}', frrconfig) + self.assertIn(f' keep-alive-timer {keep_alive_timer}', frrconfig) + self.assertIn(f' rp keep-alive-timer {rp_keep_alive_timer}', frrconfig) + self.assertIn(f' ssm prefix-list {prefix_list}', frrconfig) + self.assertIn(f' register-accept-list {prefix_list}', frrconfig) + self.assertIn(f' spt-switchover infinity-and-beyond prefix-list {prefix_list}', frrconfig) + self.assertIn(f' ecmp rebalance', frrconfig) def test_03_pim_igmp_proxy(self): igmp_proxy = ['protocols', 'igmp-proxy'] @@ -170,11 +181,11 @@ class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig(daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig() self.assertIn(f'ip igmp watermark-warn {watermark_warning}', frrconfig) for interface in interfaces: - frrconfig = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', frrconfig) self.assertIn(f' ip igmp', frrconfig) self.assertIn(f' ip igmp version {version}', frrconfig) @@ -184,9 +195,9 @@ class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase): 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) + self.assertIn(f' ip igmp join-group {join} {source}', frrconfig) else: - self.assertIn(f' ip igmp join {join}', frrconfig) + self.assertIn(f' ip igmp join-group {join}', frrconfig) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_pim6.py b/smoketest/scripts/cli/test_protocols_pim6.py index ba24edca2..4ed8fcf7a 100755 --- a/smoketest/scripts/cli/test_protocols_pim6.py +++ b/smoketest/scripts/cli/test_protocols_pim6.py @@ -17,11 +17,13 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME + from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section +from vyos.frrender import pim6_daemon from vyos.utils.process import process_named_running -PROCESS_NAME = 'pim6d' base_path = ['protocols', 'pim6'] class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): @@ -30,17 +32,19 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): # call base-classes classmethod super(TestProtocolsPIMv6, cls).setUpClass() # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same - cls.daemon_pid = process_named_running(PROCESS_NAME) + cls.daemon_pid = process_named_running(pim6_daemon) # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME def tearDown(self): self.cli_delete(base_path) self.cli_commit() # check process health and continuity - self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) + self.assertEqual(self.daemon_pid, process_named_running(pim6_daemon)) def test_pim6_01_mld_simple(self): # commit changes @@ -52,7 +56,7 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): # Verify FRR pim6d configuration for interface in interfaces: - config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', config) self.assertIn(f' ipv6 mld', config) self.assertNotIn(f' ipv6 mld version 1', config) @@ -65,7 +69,7 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): # Verify FRR pim6d configuration for interface in interfaces: - config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', config) self.assertIn(f' ipv6 mld', config) self.assertIn(f' ipv6 mld version 1', config) @@ -88,9 +92,9 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): # Verify FRR pim6d configuration for interface in interfaces: - config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', config) - self.assertIn(f' ipv6 mld join ff18::1234', config) + self.assertIn(f' ipv6 mld join-group ff18::1234', config) # Join a source-specific multicast group for interface in interfaces: @@ -100,9 +104,9 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): # Verify FRR pim6d configuration for interface in interfaces: - config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', config) - self.assertIn(f' ipv6 mld join ff38::5678 2001:db8::5678', config) + self.assertIn(f' ipv6 mld join-group ff38::5678 2001:db8::5678', config) def test_pim6_03_basic(self): interfaces = Section.interfaces('ethernet') @@ -128,14 +132,14 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): 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) + config = self.getFRRconfig('router pim6', endsection='^exit') + self.assertIn(f' join-prune-interval {join_prune_interval}', config) + self.assertIn(f' keep-alive-timer {keep_alive_timer}', config) + self.assertIn(f' packets {packets}', config) + self.assertIn(f' register-suppress-time {register_suppress_time}', config) for interface in interfaces: - config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + config = self.getFRRconfig(f'interface {interface}', endsection='^exit') 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) diff --git a/smoketest/scripts/cli/test_protocols_rip.py b/smoketest/scripts/cli/test_protocols_rip.py index bfc327fd4..27b543803 100755 --- a/smoketest/scripts/cli/test_protocols_rip.py +++ b/smoketest/scripts/cli/test_protocols_rip.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2025 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 @@ -17,11 +17,12 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.ifconfig import Section +from vyos.frrender import rip_daemon from vyos.utils.process import process_named_running -PROCESS_NAME = 'ripd' acl_in = '198' acl_out = '199' prefix_list_in = 'foo-prefix' @@ -35,10 +36,12 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase): def setUpClass(cls): super(TestProtocolsRIP, cls).setUpClass() # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same - cls.daemon_pid = process_named_running(PROCESS_NAME) + cls.daemon_pid = process_named_running(rip_daemon) # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME cls.cli_set(cls, ['policy', 'access-list', acl_in, 'rule', '10', 'action', 'permit']) cls.cli_set(cls, ['policy', 'access-list', acl_in, 'rule', '10', 'source', 'any']) @@ -66,8 +69,11 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() + frrconfig = self.getFRRconfig('router rip', endsection='^exit') + self.assertNotIn(f'router rip', frrconfig) + # check process health and continuity - self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) + self.assertEqual(self.daemon_pid, process_named_running(rip_daemon)) def test_rip_01_parameters(self): distance = '40' @@ -76,7 +82,7 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase): interfaces = Section.interfaces('ethernet') neighbors = ['1.2.3.4', '1.2.3.5', '1.2.3.6'] networks = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'] - redistribute = ['bgp', 'connected', 'isis', 'kernel', 'ospf', 'static'] + redistribute = ['bgp', 'connected', 'isis', 'kernel', 'nhrp', 'ospf', 'static'] timer_garbage = '888' timer_timeout = '1000' timer_update = '90' @@ -113,7 +119,7 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ripd configuration - frrconfig = self.getFRRconfig('router rip') + frrconfig = self.getFRRconfig('router rip', endsection='^exit') self.assertIn(f'router rip', frrconfig) self.assertIn(f' distance {distance}', frrconfig) self.assertIn(f' default-information originate', frrconfig) @@ -172,10 +178,10 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR configuration - frrconfig = self.getFRRconfig('router rip') + frrconfig = self.getFRRconfig('router rip', endsection='^exit') self.assertIn(f'version {tx_version}', frrconfig) - frrconfig = self.getFRRconfig(f'interface {interface}') + frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f' ip rip receive version {rx_version}', frrconfig) self.assertIn(f' ip rip send version {tx_version}', frrconfig) diff --git a/smoketest/scripts/cli/test_protocols_ripng.py b/smoketest/scripts/cli/test_protocols_ripng.py index 0cfb065c6..d2066b825 100755 --- a/smoketest/scripts/cli/test_protocols_ripng.py +++ b/smoketest/scripts/cli/test_protocols_ripng.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2025 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 @@ -17,11 +17,12 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.ifconfig import Section +from vyos.frrender import ripng_daemon from vyos.utils.process import process_named_running -PROCESS_NAME = 'ripngd' acl_in = '198' acl_out = '199' prefix_list_in = 'foo-prefix' @@ -36,10 +37,12 @@ class TestProtocolsRIPng(VyOSUnitTestSHIM.TestCase): # call base-classes classmethod super(TestProtocolsRIPng, cls).setUpClass() # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same - cls.daemon_pid = process_named_running(PROCESS_NAME) + cls.daemon_pid = process_named_running(ripng_daemon) # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME cls.cli_set(cls, ['policy', 'access-list6', acl_in, 'rule', '10', 'action', 'permit']) cls.cli_set(cls, ['policy', 'access-list6', acl_in, 'rule', '10', 'source', 'any']) @@ -66,8 +69,11 @@ class TestProtocolsRIPng(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() + frrconfig = self.getFRRconfig('router ripng', endsection='^exit') + self.assertNotIn(f'router ripng', frrconfig) + # check process health and continuity - self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) + self.assertEqual(self.daemon_pid, process_named_running(ripng_daemon)) def test_ripng_01_parameters(self): metric = '8' @@ -110,7 +116,7 @@ class TestProtocolsRIPng(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ripng') + frrconfig = self.getFRRconfig('router ripng', endsection='^exit') self.assertIn(f'router ripng', frrconfig) self.assertIn(f' default-information originate', frrconfig) self.assertIn(f' default-metric {metric}', frrconfig) diff --git a/smoketest/scripts/cli/test_protocols_rpki.py b/smoketest/scripts/cli/test_protocols_rpki.py index 29f03a26a..0addf7fee 100755 --- a/smoketest/scripts/cli/test_protocols_rpki.py +++ b/smoketest/scripts/cli/test_protocols_rpki.py @@ -17,14 +17,14 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.configsession import ConfigSessionError +from vyos.frrender import bgp_daemon from vyos.utils.file import read_file from vyos.utils.process import process_named_running base_path = ['protocols', 'rpki'] -PROCESS_NAME = 'bgpd' - rpki_key_name = 'rpki-smoketest' rpki_key_type = 'ssh-rsa' @@ -108,17 +108,22 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): # call base-classes classmethod super(TestProtocolsRPKI, cls).setUpClass() # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same - cls.daemon_pid = process_named_running(PROCESS_NAME) + cls.daemon_pid = process_named_running(bgp_daemon) # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME def tearDown(self): self.cli_delete(base_path) self.cli_commit() + frrconfig = self.getFRRconfig('rpki', endsection='^exit') + self.assertNotIn(f'rpki', frrconfig) + # check process health and continuity - self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) + self.assertEqual(self.daemon_pid, process_named_running(bgp_daemon)) def test_rpki(self): expire_interval = '3600' @@ -151,7 +156,7 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR configuration - frrconfig = self.getFRRconfig('rpki') + frrconfig = self.getFRRconfig('rpki', endsection='^exit') self.assertIn(f'rpki expire_interval {expire_interval}', frrconfig) self.assertIn(f'rpki polling_period {polling_period}', frrconfig) self.assertIn(f'rpki retry_interval {retry_interval}', frrconfig) @@ -159,7 +164,7 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): for peer, peer_config in cache.items(): port = peer_config['port'] preference = peer_config['preference'] - self.assertIn(f'rpki cache {peer} {port} preference {preference}', frrconfig) + self.assertIn(f'rpki cache tcp {peer} {port} preference {preference}', frrconfig) def test_rpki_ssh(self): polling = '7200' @@ -190,12 +195,12 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR configuration - frrconfig = self.getFRRconfig('rpki') + frrconfig = self.getFRRconfig('rpki', endsection='^exit') for cache_name, cache_config in cache.items(): port = cache_config['port'] preference = cache_config['preference'] username = cache_config['username'] - self.assertIn(f'rpki cache {cache_name} {port} {username} /run/frr/id_rpki_{cache_name} /run/frr/id_rpki_{cache_name}.pub preference {preference}', frrconfig) + self.assertIn(f'rpki cache ssh {cache_name} {port} {username} /run/frr/id_rpki_{cache_name} /run/frr/id_rpki_{cache_name}.pub preference {preference}', frrconfig) # Verify content of SSH keys tmp = read_file(f'/run/frr/id_rpki_{cache_name}') @@ -213,7 +218,7 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): port = cache_config['port'] preference = cache_config['preference'] username = cache_config['username'] - self.assertIn(f'rpki cache {cache_name} {port} {username} /run/frr/id_rpki_{cache_name} /run/frr/id_rpki_{cache_name}.pub preference {preference}', frrconfig) + self.assertIn(f'rpki cache ssh {cache_name} {port} {username} /run/frr/id_rpki_{cache_name} /run/frr/id_rpki_{cache_name}.pub preference {preference}', frrconfig) # Verify content of SSH keys tmp = read_file(f'/run/frr/id_rpki_{cache_name}') @@ -243,5 +248,41 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): with self.assertRaises(ConfigSessionError): self.cli_commit() + def test_rpki_source_address(self): + peer = '192.0.2.1' + port = '8080' + preference = '1' + username = 'foo' + source_address = '100.10.10.1' + + self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', f'{source_address}/24']) + + # Configure a TCP cache server + self.cli_set(base_path + ['cache', peer, 'port', port]) + self.cli_set(base_path + ['cache', peer, 'preference', preference]) + self.cli_set(base_path + ['cache', peer, 'source-address', source_address]) + self.cli_commit() + + # Verify FRR configuration + frrconfig = self.getFRRconfig('rpki') + self.assertIn(f'rpki cache tcp {peer} {port} source {source_address} preference {preference}', frrconfig) + + self.cli_set(['pki', 'openssh', rpki_key_name, 'private', 'key', rpki_ssh_key.replace('\n', '')]) + self.cli_set(['pki', 'openssh', rpki_key_name, 'public', 'key', rpki_ssh_pub.replace('\n', '')]) + self.cli_set(['pki', 'openssh', rpki_key_name, 'public', 'type', rpki_key_type]) + + # Configure a SSH cache server + self.cli_set(base_path + ['cache', peer, 'ssh', 'username', username]) + self.cli_set(base_path + ['cache', peer, 'ssh', 'key', rpki_key_name]) + self.cli_commit() + + # Verify FRR configuration + frrconfig = self.getFRRconfig('rpki') + self.assertIn( + f'rpki cache ssh {peer} {port} {username} /run/frr/id_rpki_{peer} /run/frr/id_rpki_{peer}.pub source {source_address} preference {preference}', + frrconfig, + ) + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_segment-routing.py b/smoketest/scripts/cli/test_protocols_segment-routing.py index daa7f088f..94c808733 100755 --- a/smoketest/scripts/cli/test_protocols_segment-routing.py +++ b/smoketest/scripts/cli/test_protocols_segment-routing.py @@ -17,14 +17,16 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section +from vyos.frrender import zebra_daemon from vyos.utils.process import process_named_running from vyos.utils.system import sysctl_read base_path = ['protocols', 'segment-routing'] -PROCESS_NAME = 'zebra' + class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase): @classmethod @@ -32,29 +34,81 @@ class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase): # call base-classes classmethod super(TestProtocolsSegmentRouting, cls).setUpClass() # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same - cls.daemon_pid = process_named_running(PROCESS_NAME) + cls.daemon_pid = process_named_running(zebra_daemon) # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME def tearDown(self): self.cli_delete(base_path) self.cli_commit() # check process health and continuity - self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) + self.assertEqual(self.daemon_pid, process_named_running(zebra_daemon)) def test_srv6(self): interfaces = Section.interfaces('ethernet', vlan=False) locators = { - 'foo' : { 'prefix' : '2001:a::/64' }, - 'foo' : { 'prefix' : '2001:b::/64', 'usid' : {} }, + 'foo1': {'prefix': '2001:a::/64'}, + 'foo2': {'prefix': '2001:b::/64', 'usid': {}}, + 'foo3': {'prefix': '2001:c::/64', 'format': 'uncompressed-f4024'}, + 'foo4': { + 'prefix': '2001:d::/48', + 'block-len': '32', + 'node-len': '16', + 'func-bits': '16', + 'usid': {}, + 'format': 'usid-f3216', + }, } for locator, locator_config in locators.items(): - self.cli_set(base_path + ['srv6', 'locator', locator, 'prefix', locator_config['prefix']]) + self.cli_set( + base_path + + ['srv6', 'locator', locator, 'prefix', locator_config['prefix']] + ) + if 'block-len' in locator_config: + self.cli_set( + base_path + + [ + 'srv6', + 'locator', + locator, + 'block-len', + locator_config['block-len'], + ] + ) + if 'node-len' in locator_config: + self.cli_set( + base_path + + [ + 'srv6', + 'locator', + locator, + 'node-len', + locator_config['node-len'], + ] + ) + if 'func-bits' in locator_config: + self.cli_set( + base_path + + [ + 'srv6', + 'locator', + locator, + 'func-bits', + locator_config['func-bits'], + ] + ) if 'usid' in locator_config: self.cli_set(base_path + ['srv6', 'locator', locator, 'behavior-usid']) + if 'format' in locator_config: + self.cli_set( + base_path + + ['srv6', 'locator', locator, 'format', locator_config['format']] + ) # verify() - SRv6 should be enabled on at least one interface! with self.assertRaises(ConfigSessionError): @@ -65,16 +119,33 @@ class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase): self.cli_commit() for interface in interfaces: - self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_enabled'), '1') - self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_require_hmac'), '0') # default - - frrconfig = self.getFRRconfig(f'segment-routing', daemon='zebra') - self.assertIn(f'segment-routing', frrconfig) - self.assertIn(f' srv6', frrconfig) - self.assertIn(f' locators', frrconfig) + self.assertEqual( + sysctl_read(f'net.ipv6.conf.{interface}.seg6_enabled'), '1' + ) + self.assertEqual( + sysctl_read(f'net.ipv6.conf.{interface}.seg6_require_hmac'), '0' + ) # default + + frrconfig = self.getFRRconfig('segment-routing', endsection='^exit') + self.assertIn('segment-routing', frrconfig) + self.assertIn(' srv6', frrconfig) + self.assertIn(' locators', frrconfig) for locator, locator_config in locators.items(): + prefix = locator_config['prefix'] + block_len = locator_config.get('block-len', '40') + node_len = locator_config.get('node-len', '24') + func_bits = locator_config.get('func-bits', '16') + self.assertIn(f' locator {locator}', frrconfig) - self.assertIn(f' prefix {locator_config["prefix"]} block-len 40 node-len 24 func-bits 16', frrconfig) + self.assertIn( + f' prefix {prefix} block-len {block_len} node-len {node_len} func-bits {func_bits}', + frrconfig, + ) + + if 'format' in locator_config: + self.assertIn(f' format {locator_config["format"]}', frrconfig) + if 'usid' in locator_config: + self.assertIn(' behavior usid', frrconfig) def test_srv6_sysctl(self): interfaces = Section.interfaces('ethernet', vlan=False) @@ -86,8 +157,12 @@ class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase): self.cli_commit() for interface in interfaces: - self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_enabled'), '1') - self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_require_hmac'), '-1') # ignore + self.assertEqual( + sysctl_read(f'net.ipv6.conf.{interface}.seg6_enabled'), '1' + ) + self.assertEqual( + sysctl_read(f'net.ipv6.conf.{interface}.seg6_require_hmac'), '-1' + ) # ignore # HMAC drop for interface in interfaces: @@ -96,8 +171,12 @@ class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase): self.cli_commit() for interface in interfaces: - self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_enabled'), '1') - self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_require_hmac'), '1') # drop + self.assertEqual( + sysctl_read(f'net.ipv6.conf.{interface}.seg6_enabled'), '1' + ) + self.assertEqual( + sysctl_read(f'net.ipv6.conf.{interface}.seg6_require_hmac'), '1' + ) # drop # Disable SRv6 on first interface first_if = interfaces[-1] @@ -106,5 +185,6 @@ class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase): self.assertEqual(sysctl_read(f'net.ipv6.conf.{first_if}.seg6_enabled'), '0') + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_static.py b/smoketest/scripts/cli/test_protocols_static.py index f676e2a52..79d6b3af4 100755 --- a/smoketest/scripts/cli/test_protocols_static.py +++ b/smoketest/scripts/cli/test_protocols_static.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright (C) 2021-2025 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 @@ -14,14 +14,20 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import os import unittest +from time import sleep from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.configsession import ConfigSessionError from vyos.template import is_ipv6 +from vyos.template import get_dhcp_router from vyos.utils.network import get_interface_config from vyos.utils.network import get_vrf_tableid +from vyos.utils.process import process_named_running +from vyos.xml_ref import default_value base_path = ['protocols', 'static'] vrf_path = ['protocols', 'vrf'] @@ -33,7 +39,11 @@ routes = { '192.0.2.110' : { 'distance' : '110', 'interface' : 'eth0' }, '192.0.2.120' : { 'distance' : '120', 'disable' : '' }, '192.0.2.130' : { 'bfd' : '' }, - '192.0.2.140' : { 'bfd_source' : '192.0.2.10' }, + '192.0.2.131' : { 'bfd' : '', + 'bfd_profile' : 'vyos1' }, + '192.0.2.140' : { 'bfd' : '', + 'bfd_source' : '192.0.2.10', + 'bfd_profile' : 'vyos2' }, }, 'interface' : { 'eth0' : { 'distance' : '130' }, @@ -114,22 +124,65 @@ routes = { }, } +multicast_routes = { + '224.0.0.0/24' : { + 'next_hop' : { + '224.203.0.1' : { }, + '224.203.0.2' : { 'distance' : '110'}, + }, + }, + '224.1.0.0/24' : { + 'next_hop' : { + '224.205.0.1' : { 'disable' : {} }, + '224.205.0.2' : { 'distance' : '110'}, + }, + }, + '224.2.0.0/24' : { + 'next_hop' : { + '1.2.3.0' : { }, + '1.2.3.1' : { 'distance' : '110'}, + }, + }, + '224.10.0.0/24' : { + 'interface' : { + 'eth1' : { 'disable' : {} }, + 'eth2' : { 'distance' : '110'}, + }, + }, + '224.11.0.0/24' : { + 'interface' : { + 'eth0' : { }, + 'eth1' : { 'distance' : '10'}, + }, + }, + '224.12.0.0/24' : { + 'interface' : { + 'eth0' : { }, + 'eth1' : { 'distance' : '200'}, + }, + }, +} + tables = ['80', '81', '82'] class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestProtocolsStatic, cls).setUpClass() + cls.cli_delete(cls, base_path) cls.cli_delete(cls, ['vrf']) - cls.cli_set(cls, ['vrf', 'name', 'black', 'table', '43210']) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME @classmethod def tearDownClass(cls): + cls.cli_delete(cls, base_path) cls.cli_delete(cls, ['vrf']) super(TestProtocolsStatic, cls).tearDownClass() def tearDown(self): self.cli_delete(base_path) + self.cli_delete(['vrf']) self.cli_commit() v4route = self.getFRRconfig('ip route', end='') @@ -138,7 +191,7 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): self.assertFalse(v6route) def test_01_static(self): - bfd_profile = 'vyos-test' + self.cli_set(['vrf', 'name', 'black', 'table', '43210']) for route, route_config in routes.items(): route_type = 'route' if is_ipv6(route): @@ -156,9 +209,11 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): if 'vrf' in next_hop_config: self.cli_set(base + ['next-hop', next_hop, 'vrf', next_hop_config['vrf']]) if 'bfd' in next_hop_config: - self.cli_set(base + ['next-hop', next_hop, 'bfd', 'profile', bfd_profile ]) - if 'bfd_source' in next_hop_config: - self.cli_set(base + ['next-hop', next_hop, 'bfd', 'multi-hop', 'source', next_hop_config['bfd_source'], 'profile', bfd_profile]) + self.cli_set(base + ['next-hop', next_hop, 'bfd']) + if 'bfd_profile' in next_hop_config: + self.cli_set(base + ['next-hop', next_hop, 'bfd', 'profile', next_hop_config['bfd_profile']]) + if 'bfd_source' in next_hop_config: + self.cli_set(base + ['next-hop', next_hop, 'bfd', 'multi-hop', 'source-address', next_hop_config['bfd_source']]) if 'segments' in next_hop_config: self.cli_set(base + ['next-hop', next_hop, 'segments', next_hop_config['segments']]) @@ -217,9 +272,11 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): if 'vrf' in next_hop_config: tmp += ' nexthop-vrf ' + next_hop_config['vrf'] if 'bfd' in next_hop_config: - tmp += ' bfd profile ' + bfd_profile - if 'bfd_source' in next_hop_config: - tmp += ' bfd multi-hop source ' + next_hop_config['bfd_source'] + ' profile ' + bfd_profile + tmp += ' bfd' + if 'bfd_source' in next_hop_config: + tmp += ' multi-hop source ' + next_hop_config['bfd_source'] + if 'bfd_profile' in next_hop_config: + tmp += ' profile ' + next_hop_config['bfd_profile'] if 'segments' in next_hop_config: tmp += ' segments ' + next_hop_config['segments'] @@ -269,6 +326,7 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): self.assertIn(tmp, frrconfig) def test_02_static_table(self): + self.cli_set(['vrf', 'name', 'black', 'table', '43210']) for table in tables: for route, route_config in routes.items(): route_type = 'route' @@ -363,6 +421,7 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): def test_03_static_vrf(self): + self.cli_set(['vrf', 'name', 'black', 'table', '43210']) # Create VRF instances and apply the static routes from above to FRR. # Re-read the configured routes and match them if they are programmed # properly. This also includes VRF leaking @@ -426,7 +485,7 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): self.assertEqual(tmp['linkinfo']['info_kind'], 'vrf') # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'vrf {vrf}') + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertIn(f'vrf {vrf}', frrconfig) # Verify routes @@ -478,5 +537,87 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): self.assertIn(tmp, frrconfig) + def test_04_static_multicast(self): + for route, route_config in multicast_routes.items(): + if 'next_hop' in route_config: + base = base_path + ['mroute', route] + for next_hop, next_hop_config in route_config['next_hop'].items(): + self.cli_set(base + ['next-hop', next_hop]) + if 'distance' in next_hop_config: + self.cli_set(base + ['next-hop', next_hop, 'distance', next_hop_config['distance']]) + if 'disable' in next_hop_config: + self.cli_set(base + ['next-hop', next_hop, 'disable']) + + if 'interface' in route_config: + base = base_path + ['mroute', route] + for next_hop, next_hop_config in route_config['interface'].items(): + self.cli_set(base + ['interface', next_hop]) + if 'distance' in next_hop_config: + self.cli_set(base + ['interface', next_hop, 'distance', next_hop_config['distance']]) + + self.cli_commit() + + # Verify FRR configuration + frrconfig = self.getFRRconfig('ip mroute', end='') + for route, route_config in multicast_routes.items(): + if 'next_hop' in route_config: + for next_hop, next_hop_config in route_config['next_hop'].items(): + tmp = f'ip mroute {route} {next_hop}' + if 'distance' in next_hop_config: + tmp += ' ' + next_hop_config['distance'] + if 'disable' in next_hop_config: + self.assertNotIn(tmp, frrconfig) + else: + self.assertIn(tmp, frrconfig) + + if 'next_hop_interface' in route_config: + for next_hop, next_hop_config in route_config['next_hop_interface'].items(): + tmp = f'ip mroute {route} {next_hop}' + if 'distance' in next_hop_config: + tmp += ' ' + next_hop_config['distance'] + if 'disable' in next_hop_config: + self.assertNotIn(tmp, frrconfig) + else: + self.assertIn(tmp, frrconfig) + + def test_05_dhcp_default_route(self): + # When running via vyos-build under the QEmu environment a local DHCP + # server is available. This test verifies that the default route is set. + # When not running under the VyOS QEMU environment, this test is skipped. + if not os.path.exists('/tmp/vyos.smoketests.hint'): + self.skipTest('Not running under VyOS CI/CD QEMU environment!') + + interface = 'eth0' + interface_path = ['interfaces', 'ethernet', interface] + default_distance = default_value(interface_path + ['dhcp-options', 'default-route-distance']) + self.cli_set(interface_path + ['address', 'dhcp']) + self.cli_commit() + + # Wait for dhclient to receive IP address and default gateway + sleep(5) + + router = get_dhcp_router(interface) + frrconfig = self.getFRRconfig('') + self.assertIn(rf'ip route 0.0.0.0/0 {router} {interface} tag 210 {default_distance}', frrconfig) + + # T6991: Default route is missing when there is no "protocols static" + # CLI node entry + self.cli_delete(base_path) + # We can trigger a FRR reconfiguration and config re-rendering when + # we simply disable IPv6 forwarding + self.cli_set(['system', 'ipv6', 'disable-forwarding']) + self.cli_commit() + + # Re-check FRR configuration that default route is still present + frrconfig = self.getFRRconfig('') + self.assertIn(rf'ip route 0.0.0.0/0 {router} {interface} tag 210 {default_distance}', frrconfig) + + self.cli_delete(interface_path + ['address']) + self.cli_commit() + + # Wait for dhclient to stop + while process_named_running('dhclient', cmdline=interface, timeout=10): + sleep(0.250) + if __name__ == '__main__': - unittest.main(verbosity=2, failfast=True) + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_static_multicast.py b/smoketest/scripts/cli/test_protocols_static_multicast.py deleted file mode 100755 index 9fdda236f..000000000 --- a/smoketest/scripts/cli/test_protocols_static_multicast.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2024 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 - - -base_path = ['protocols', 'static', 'multicast'] - - -class TestProtocolsStaticMulticast(VyOSUnitTestSHIM.TestCase): - - def tearDown(self): - self.cli_delete(base_path) - self.cli_commit() - - mroute = self.getFRRconfig('ip mroute', end='') - self.assertFalse(mroute) - - def test_01_static_multicast(self): - - self.cli_set(base_path + ['route', '224.202.0.0/24', 'next-hop', '224.203.0.1']) - self.cli_set(base_path + ['interface-route', '224.203.0.0/24', 'next-hop-interface', 'eth0']) - - self.cli_commit() - - # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig('ip mroute', end='') - - self.assertIn('ip mroute 224.202.0.0/24 224.203.0.1', frrconfig) - self.assertIn('ip mroute 224.203.0.0/24 eth0', frrconfig) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_qos.py b/smoketest/scripts/cli/test_qos.py index b98c0e9b7..231743344 100755 --- a/smoketest/scripts/cli/test_qos.py +++ b/smoketest/scripts/cli/test_qos.py @@ -21,30 +21,49 @@ from json import loads from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError -from vyos.ifconfig import Section +from vyos.ifconfig import Section, Interface +from vyos.qos import CAKE from vyos.utils.process import cmd base_path = ['qos'] -def get_tc_qdisc_json(interface) -> dict: + +def get_tc_qdisc_json(interface, all=False) -> dict: tmp = cmd(f'tc -detail -json qdisc show dev {interface}') tmp = loads(tmp) + + if all: + return tmp + return next(iter(tmp)) -def get_tc_filter_json(interface, direction) -> list: - if direction not in ['ingress', 'egress']: + +def get_tc_filter_json(interface, direction=None) -> list: + if direction not in ['ingress', 'egress', None]: raise ValueError() - tmp = cmd(f'tc -detail -json filter show dev {interface} {direction}') + + cmd_stmt = f'tc -detail -json filter show dev {interface}' + if direction: + cmd_stmt += f' {direction}' + + tmp = cmd(cmd_stmt) tmp = loads(tmp) return tmp -def get_tc_filter_details(interface, direction) -> list: + +def get_tc_filter_details(interface, direction=None) -> list: # json doesn't contain all params, such as mtu - if direction not in ['ingress', 'egress']: + if direction not in ['ingress', 'egress', None]: raise ValueError() - tmp = cmd(f'tc -details filter show dev {interface} {direction}') + + cmd_stmt = f'tc -details filter show dev {interface}' + if direction: + cmd_stmt += f' {direction}' + + tmp = cmd(cmd_stmt) return tmp + class TestQoS(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): @@ -854,6 +873,436 @@ class TestQoS(VyOSUnitTestSHIM.TestCase): self.cli_set(['qos', 'traffic-match-group', '3', 'match-group', 'unexpected']) self.cli_commit() + def test_17_cake_updates(self): + bandwidth = 1000000 + rtt = 200 + interface = self._interfaces[0] + policy_name = f'qos-policy-{interface}' + + self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) + self.cli_set( + base_path + ['policy', 'cake', policy_name, 'bandwidth', str(bandwidth)] + ) + self.cli_set(base_path + ['policy', 'cake', policy_name, 'rtt', str(rtt)]) + + # commit changes + self.cli_commit() + + tmp = get_tc_qdisc_json(interface) + + self.assertEqual('cake', tmp['kind']) + # TC store rates as a 32-bit unsigned integer in bps (Bytes per second) + self.assertEqual(int(bandwidth * 125), tmp['options']['bandwidth']) + # RTT internally is in us + self.assertEqual(int(rtt * 1000), tmp['options']['rtt']) + self.assertEqual('triple-isolate', tmp['options']['flowmode']) + self.assertFalse(tmp['options']['ingress']) + self.assertFalse(tmp['options']['nat']) + self.assertTrue(tmp['options']['raw']) + + nat = True + for flow_isolation in [ + 'blind', + 'src-host', + 'dst-host', + 'dual-dst-host', + 'dual-src-host', + 'triple-isolate', + 'flow', + 'host', + ]: + self.cli_set( + base_path + + ['policy', 'cake', policy_name, 'flow-isolation', flow_isolation] + ) + + if nat: + self.cli_set( + base_path + ['policy', 'cake', policy_name, 'flow-isolation-nat'] + ) + else: + self.cli_delete( + base_path + ['policy', 'cake', policy_name, 'flow-isolation-nat'] + ) + + self.cli_commit() + + tmp = get_tc_qdisc_json(interface) + self.assertEqual( + CAKE.flow_isolation_map.get(flow_isolation), tmp['options']['flowmode'] + ) + + self.assertEqual(nat, tmp['options']['nat']) + nat = not nat + + def test_18_priority_queue_default(self): + interface = self._interfaces[0] + policy_name = f'qos-policy-{interface}' + + self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) + self.cli_set( + base_path + + ['policy', 'priority-queue', policy_name, 'description', 'default policy'] + ) + + self.cli_commit() + + tmp = get_tc_qdisc_json(interface, all=True) + + self.assertEqual(2, len(tmp)) + self.assertEqual('prio', tmp[0]['kind']) + self.assertDictEqual( + { + 'bands': 2, + 'priomap': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + 'multiqueue': False, + }, + tmp[0]['options'], + ) + self.assertEqual('pfifo', tmp[1]['kind']) + self.assertDictEqual({'limit': 1000}, tmp[1]['options']) + + def test_19_priority_queue_default_random_detect(self): + interface = self._interfaces[0] + policy_name = f'qos-policy-{interface}' + + self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) + self.cli_set( + base_path + + [ + 'policy', + 'priority-queue', + policy_name, + 'default', + 'queue-type', + 'random-detect', + ] + ) + + self.cli_commit() + + tmp = get_tc_qdisc_json(interface, all=True) + + self.assertEqual(2, len(tmp)) + self.assertEqual('prio', tmp[0]['kind']) + self.assertDictEqual( + { + 'bands': 2, + 'priomap': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + 'multiqueue': False, + }, + tmp[0]['options'], + ) + self.assertEqual('red', tmp[1]['kind']) + self.assertDictEqual( + { + 'limit': 73728, + 'min': 9216, + 'max': 18432, + 'ecn': False, + 'harddrop': False, + 'adaptive': False, + 'nodrop': False, + 'ewma': 3, + 'probability': 0.1, + 'Scell_log': 13, + }, + tmp[1]['options'], + ) + + def test_20_round_robin_policy_default(self): + interface = self._interfaces[0] + policy_name = f'qos-policy-{interface}' + + self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) + self.cli_set( + base_path + + ['policy', 'round-robin', policy_name, 'description', 'default policy'] + ) + + # commit changes + self.cli_commit() + + tmp = get_tc_qdisc_json(interface, all=True) + + self.assertEqual(2, len(tmp)) + self.assertEqual('drr', tmp[0]['kind']) + self.assertDictEqual({}, tmp[0]['options']) + self.assertEqual('sfq', tmp[1]['kind']) + self.assertDictEqual( + { + 'limit': 127, + 'quantum': 1514, + 'depth': 127, + 'flows': 128, + 'divisor': 1024, + }, + tmp[1]['options'], + ) + + tmp = get_tc_filter_json(interface) + self.assertEqual(3, len(tmp)) + + for rec in tmp: + self.assertEqual('u32', rec['kind']) + self.assertEqual(1, rec['pref']) + self.assertEqual('all', rec['protocol']) + + self.assertDictEqual( + { + 'fh': '800::800', + 'order': 2048, + 'key_ht': '800', + 'bkt': '0', + 'flowid': '1:1', + 'not_in_hw': True, + 'match': {'value': '0', 'mask': '0', 'offmask': '', 'off': 0}, + }, + tmp[2]['options'], + ) + + def test_21_shaper_hfsc(self): + interface = self._interfaces[0] + policy_name = f'qos-policy-{interface}' + ul = { + 'm1': '100kbit', + 'm2': '150kbit', + 'd': '100', + } + ls = {'m2': '120kbit'} + rt = { + 'm1': '110kbit', + 'm2': '130kbit', + 'd': '75', + } + self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) + self.cli_set(base_path + ['policy', 'shaper-hfsc', policy_name]) + + # Policy {policy_name} misses "default" class! + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'default', 'upperlimit'] + ) + + # At least one m2 value needs to be set for class: {class_name} + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'default', 'upperlimit', 'm1', ul['m1']] + ) + # {class_name} upperlimit m1 value is set, but no m2 was found! + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'default', 'upperlimit', 'm2', ul['m2']] + ) + # {class_name} upperlimit m1 value is set, but no d was found! + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'default', 'upperlimit', 'd', ul['d']] + ) + # Linkshare m2 needs to be defined to use upperlimit m2 for class: {class_name} + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'default', 'linkshare', 'm2', ls['m2']] + ) + self.cli_commit() + + # use raw because tc json is incorrect here + tmp = cmd(f'tc -details qdisc show dev {interface}') + for rec in tmp.split('\n'): + rec = rec.strip() + if 'root' in rec: + self.assertEqual(rec, 'qdisc hfsc 1: root refcnt 2 default 2') + else: + self.assertRegex( + rec, + r'qdisc sfq \S+: parent 1:2 limit 127p quantum 1514b depth 127 flows 128 divisor 1024 perturb 10sec', + ) + # use raw because tc json is incorrect here + tmp = cmd(f'tc -details class show dev {interface}') + for rec in tmp.split('\n'): + rec = rec.strip().lower() + if 'root' in rec: + self.assertEqual(rec, 'class hfsc 1: root') + elif 'hfsc 1:1' in rec: + # m2 \S+bit is auto bandwidth + self.assertRegex( + rec, + r'class hfsc 1:1 parent 1: sc m1 0bit d 0us m2 \S+bit ul m1 0bit d 0us m2 \S+bit', + ) + else: + self.assertRegex( + rec, + rf'class hfsc 1:2 parent 1:1 leaf \S+: ls m1 0bit d 0us m2 {ls["m2"]} ul m1 {ul["m1"]} d {ul["d"]}ms m2 {ul["m2"]}', + ) + + for key, val in rt.items(): + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'default', 'realtime', key, val] + ) + self.cli_commit() + + tmp = cmd(f'tc -details class show dev {interface}') + for rec in tmp.split('\n'): + rec = rec.strip().lower() + if 'hfsc 1:2' in rec: + self.assertTrue( + f'rt m1 {rt["m1"]} d {rt["d"]}ms m2 {rt["m2"]} ls m1 0bit d 0us m2 {ls["m2"]} ul m1 {ul["m1"]} d {ul["d"]}ms m2 {ul["m2"]}' + in rec + ) + + # add some class + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'class', '10', 'linkshare', 'm2', '300kbit'] + ) + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'class', '10', 'match', 'tst', 'ip', 'dscp', 'internet'] + ) + + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'class', '30', 'realtime', 'm2', '250kbit'] + ) + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'class', '30', 'realtime', 'd', '77'] + ) + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'class', '30', 'match', 'tst30', 'ip', 'dscp', 'critical'] + ) + self.cli_commit() + + tmp = cmd(f'tc -details qdisc show dev {interface}') + self.assertEqual(4, len(tmp.split('\n'))) + + tmp = cmd(f'tc -details class show dev {interface}') + tmp = tmp.lower() + + self.assertTrue( + f'rt m1 {rt["m1"]} d {rt["d"]}ms m2 {rt["m2"]} ls m1 0bit d 0us m2 {ls["m2"]} ul m1 {ul["m1"]} d {ul["d"]}ms m2 {ul["m2"]}' + in tmp + ) + self.assertTrue(': ls m1 0bit d 0us m2 300kbit' in tmp) + self.assertTrue(': rt m1 0bit d 77ms m2 250kbit' in tmp) + + def test_22_rate_control_default(self): + interface = self._interfaces[0] + policy_name = f'qos-policy-{interface}' + bandwidth = 5000 + + self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) + self.cli_set(base_path + ['policy', 'rate-control', policy_name]) + with self.assertRaises(ConfigSessionError): + # Bandwidth not defined + self.cli_commit() + + self.cli_set(base_path + ['policy', 'rate-control', policy_name, 'bandwidth', str(bandwidth)]) + # commit changes + self.cli_commit() + + tmp = get_tc_qdisc_json(interface) + + self.assertEqual('tbf', tmp['kind']) + # TC store rates as a 32-bit unsigned integer in bps (Bytes per second) + self.assertEqual(int(bandwidth * 125), tmp['options']['rate']) + + def test_23_policy_limiter_iif_filter(self): + policy_name = 'smoke_test' + base_policy_path = ['qos', 'policy', 'limiter', policy_name] + + self.cli_set(['qos', 'interface', self._interfaces[0], 'ingress', policy_name]) + self.cli_set(base_policy_path + ['class', '100', 'bandwidth', '20gbit']) + self.cli_set(base_policy_path + ['class', '100', 'burst', '3760k']) + self.cli_set(base_policy_path + ['class', '100', 'match', 'test', 'interface', self._interfaces[0]]) + self.cli_set(base_policy_path + ['class', '100', 'priority', '20']) + self.cli_set(base_policy_path + ['default', 'bandwidth', '1gbit']) + self.cli_set(base_policy_path + ['default', 'burst', '125000000b']) + self.cli_commit() + + iif = Interface(self._interfaces[0]).get_ifindex() + tc_filters = cmd(f'tc filter show dev {self._interfaces[0]} ingress') + + # class 100 + self.assertIn('filter parent ffff: protocol all pref 20 basic chain 0', tc_filters) + self.assertIn(f'meta(rt_iif eq {iif})', tc_filters) + self.assertIn('action order 1: police 0x1 rate 20Gbit burst 3847500b mtu 2Kb action drop overhead 0b', tc_filters) + # default + self.assertIn('filter parent ffff: protocol all pref 255 basic chain 0', tc_filters) + self.assertIn('action order 1: police 0x2 rate 1Gbit burst 125000000b mtu 2Kb action drop overhead 0b', tc_filters) + + def test_24_policy_shaper_match_ether(self): + interface = self._interfaces[0] + bandwidth = 250 + default_bandwidth = 20 + default_ceil = 30 + class_bandwidth = 50 + class_ceil = 80 + + shaper_name = f'qos-shaper-{interface}' + + self.cli_set(base_path + ['interface', interface, 'egress', shaper_name]) + self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'bandwidth', f'{bandwidth}mbit']) + self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'bandwidth', f'{default_bandwidth}mbit']) + self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'ceiling', f'{default_ceil}mbit']) + self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'queue-type', 'fair-queue']) + self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '23', 'bandwidth', f'{class_bandwidth}mbit']) + self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '23', 'ceiling', f'{class_ceil}mbit']) + self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '23', 'match', '10', 'ether', 'protocol', 'all']) + self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '23', 'match', '10', 'ether', 'destination', '0c:89:0a:2e:00:00']) + self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '23', 'match', '10', 'ether', 'source', '0c:89:0a:2e:00:01']) + + # commit changes + self.cli_commit() + + config_entries = ( + f'root rate {bandwidth}Mbit ceil {bandwidth}Mbit', + f'prio 0 rate {class_bandwidth}Mbit ceil {class_ceil}Mbit', + f'prio 7 rate {default_bandwidth}Mbit ceil {default_ceil}Mbit' + ) + + output = cmd(f'tc class show dev {interface}') + + for config_entry in config_entries: + self.assertIn(config_entry, output) + + filter = get_tc_filter_details(interface) + self.assertIn('match 0c890a2e/ffffffff at -8', filter) + self.assertIn('match 00010000/ffff0000 at -4', filter) + self.assertIn('match 00000c89/0000ffff at -16', filter) + self.assertIn('match 0a2e0000/ffffffff at -12', filter) + + for proto in ['802.1Q', '802_2', '802_3', 'aarp', 'aoe', 'arp', 'atalk', + 'dec', 'ip', 'ipv6', 'ipx', 'lat', 'localtalk', 'rarp', + 'snap', 'x25', 1, 255, 65535]: + self.cli_set( + base_path + ['policy', 'shaper', shaper_name, 'class', '23', + 'match', '10', 'ether', 'protocol', str(proto)]) + self.cli_commit() + + if isinstance(proto, int): + if proto == 1: + self.assertIn(f'filter parent 1: protocol 802_3 pref', + get_tc_filter_details(interface)) + else: + self.assertIn(f'filter parent 1: protocol [{proto}] pref', + get_tc_filter_details(interface)) + + elif proto == '0x000C': + # see other codes in the iproute2 eg https://github.com/iproute2/iproute2/blob/413cf4f03a9b6a219c94b86f41d67992b0a14b82/include/uapi/linux/if_ether.h#L130 + self.assertIn(f'filter parent 1: protocol can pref', + get_tc_filter_details(interface)) + + else: + self.assertIn(f'filter parent 1: protocol {proto} pref', + get_tc_filter_details(interface)) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py index 46c4e25a1..e421f04d2 100755 --- a/smoketest/scripts/cli/test_service_dhcp-server.py +++ b/smoketest/scripts/cli/test_service_dhcp-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright (C) 2020-2025 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 @@ -15,6 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os +import re import unittest from json import loads @@ -22,15 +23,21 @@ from json import loads from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError +from vyos.kea import kea_add_lease +from vyos.kea import kea_delete_lease +from vyos.utils.process import cmd from vyos.utils.process import process_named_running from vyos.utils.file import read_file from vyos.template import inc_ip from vyos.template import dec_ip PROCESS_NAME = 'kea-dhcp4' +D2_PROCESS_NAME = 'kea-dhcp-ddns' CTRL_PROCESS_NAME = 'kea-ctrl-agent' KEA4_CONF = '/run/kea/kea-dhcp4.conf' +KEA4_D2_CONF = '/run/kea/kea-dhcp-ddns.conf' KEA4_CTRL = '/run/kea/dhcp4-ctrl-socket' +HOSTSD_CLIENT = '/usr/bin/vyos-hostsd-client' base_path = ['service', 'dhcp-server'] interface = 'dum8765' subnet = '192.0.2.0/25' @@ -39,15 +46,18 @@ dns_1 = inc_ip(subnet, 2) dns_2 = inc_ip(subnet, 3) domain_name = 'vyos.net' + class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestServiceDHCPServer, cls).setUpClass() - # Clear out current configuration to allow running this test on a live system + # Clear out current configuration to allow running this test on a live system cls.cli_delete(cls, base_path) cidr_mask = subnet.split('/')[-1] - cls.cli_set(cls, ['interfaces', 'dummy', interface, 'address', f'{router}/{cidr_mask}']) + cls.cli_set( + cls, ['interfaces', 'dummy', interface, 'address', f'{router}/{cidr_mask}'] + ) @classmethod def tearDownClass(cls): @@ -69,7 +79,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.assertTrue(isinstance(current, list), msg=f'Failed path: {path}') self.assertTrue(0 <= key < len(current), msg=f'Failed path: {path}') else: - assert False, "Invalid type" + assert False, 'Invalid type' current = current[key] @@ -88,19 +98,26 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.assertTrue(key in base_obj) self.assertEqual(base_obj[key], value) + def verify_service_running(self): + tmp = cmd('tail -n 100 /var/log/messages | grep kea') + self.assertTrue(process_named_running(PROCESS_NAME), msg=f'Service not running, log: {tmp}') + def test_dhcp_single_pool_range(self): shared_net_name = 'SMOKE-1' range_0_start = inc_ip(subnet, 10) - range_0_stop = inc_ip(subnet, 20) + range_0_stop = inc_ip(subnet, 20) range_1_start = inc_ip(subnet, 40) - range_1_stop = inc_ip(subnet, 50) + range_1_stop = inc_ip(subnet, 50) self.cli_set(base_path + ['listen-interface', interface]) + self.cli_set(base_path + ['shared-network-name', shared_net_name, 'ping-check']) + pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] self.cli_set(pool + ['subnet-id', '1']) self.cli_set(pool + ['ignore-client-id']) + self.cli_set(pool + ['ping-check']) # we use the first subnet IP address as default gateway self.cli_set(pool + ['option', 'default-router', router]) self.cli_set(pool + ['option', 'name-server', dns_1]) @@ -121,55 +138,90 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): config = read_file(KEA4_CONF) obj = loads(config) - self.verify_config_value(obj, ['Dhcp4', 'interfaces-config'], 'interfaces', [interface]) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'match-client-id', False) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400) + self.verify_config_value( + obj, ['Dhcp4', 'interfaces-config'], 'interfaces', [interface] + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1 + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'match-client-id', False + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400 + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400 + ) + + # Verify ping-check + self.verify_config_value( + obj, + ['Dhcp4', 'shared-networks', 0, 'user-context'], + 'enable-ping-check', + True + ) + + self.verify_config_value( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'user-context'], + 'enable-ping-check', + True + ) # Verify options self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'domain-name', 'data': domain_name}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-name', 'data': domain_name}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'routers', 'data': router}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}, + ) # Verify pools self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], - {'pool': f'{range_0_start} - {range_0_stop}'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{range_0_start} - {range_0_stop}'}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], - {'pool': f'{range_1_start} - {range_1_stop}'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{range_1_start} - {range_1_stop}'}, + ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_single_pool_options(self): shared_net_name = 'SMOKE-0815' - range_0_start = inc_ip(subnet, 10) - range_0_stop = inc_ip(subnet, 20) - smtp_server = '1.2.3.4' - time_server = '4.3.2.1' - tftp_server = 'tftp.vyos.io' - search_domains = ['foo.vyos.net', 'bar.vyos.net'] - bootfile_name = 'vyos' - bootfile_server = '192.0.2.1' - wpad = 'http://wpad.vyos.io/foo/bar' - server_identifier = bootfile_server + range_0_start = inc_ip(subnet, 10) + range_0_stop = inc_ip(subnet, 20) + smtp_server = '1.2.3.4' + time_server = '4.3.2.1' + tftp_server = 'tftp.vyos.io' + search_domains = ['foo.vyos.net', 'bar.vyos.net'] + bootfile_name = 'vyos' + bootfile_server = '192.0.2.1' + wpad = 'http://wpad.vyos.io/foo/bar' + server_identifier = bootfile_server ipv6_only_preferred = '300' + capwap_access_controller = '192.168.2.125' pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] self.cli_set(pool + ['subnet-id', '1']) @@ -189,8 +241,16 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.cli_set(pool + ['option', 'bootfile-server', bootfile_server]) self.cli_set(pool + ['option', 'wpad-url', wpad]) self.cli_set(pool + ['option', 'server-identifier', server_identifier]) + self.cli_set( + pool + ['option', 'capwap-controller', capwap_access_controller] + ) + + static_route = '10.0.0.0/24' + static_route_nexthop = '192.0.2.1' - self.cli_set(pool + ['option', 'static-route', '10.0.0.0/24', 'next-hop', '192.0.2.1']) + self.cli_set( + pool + ['option', 'static-route', static_route, 'next-hop', static_route_nexthop] + ) self.cli_set(pool + ['option', 'ipv6-only-preferred', ipv6_only_preferred]) self.cli_set(pool + ['option', 'time-zone', 'Europe/London']) @@ -203,95 +263,133 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): config = read_file(KEA4_CONF) obj = loads(config) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'boot-file-name', bootfile_name) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'next-server', bootfile_server) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet + ) + self.verify_config_value( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4'], + 'boot-file-name', + bootfile_name, + ) + self.verify_config_value( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4'], + 'next-server', + bootfile_server, + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400 + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400 + ) # Verify options self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'domain-name', 'data': domain_name}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-name', 'data': domain_name}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'domain-search', 'data': ', '.join(search_domains)}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-search', 'data': ', '.join(search_domains)}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'pop-server', 'data': smtp_server}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'pop-server', 'data': smtp_server}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'smtp-server', 'data': smtp_server}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'smtp-server', 'data': smtp_server}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'time-servers', 'data': time_server}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'time-servers', 'data': time_server}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'routers', 'data': router}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'dhcp-server-identifier', 'data': server_identifier}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'dhcp-server-identifier', 'data': server_identifier}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'tftp-server-name', 'data': tftp_server}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'capwap-ac-v4', 'data': capwap_access_controller}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'wpad-url', 'data': wpad}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'tftp-server-name', 'data': tftp_server}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'rfc3442-static-route', 'data': '24,10,0,0,192,0,2,1, 0,192,0,2,1'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'wpad-url', 'data': wpad}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'windows-static-route', 'data': '24,10,0,0,192,0,2,1'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + { + 'name': 'classless-static-route', + 'data': f'{static_route} - {static_route_nexthop}, 0.0.0.0/0 - {router}', + }, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'v6-only-preferred', 'data': ipv6_only_preferred}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'v6-only-preferred', 'data': ipv6_only_preferred}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'ip-forwarding', 'data': "true"}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'ip-forwarding', 'data': 'true'}, + ) # Time zone self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'pcode', 'data': 'GMT0BST,M3.5.0/1,M10.5.0'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'pcode', 'data': 'GMT0BST,M3.5.0/1,M10.5.0'}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'tcode', 'data': 'Europe/London'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'tcode', 'data': 'Europe/London'}, + ) # Verify pools self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], - {'pool': f'{range_0_start} - {range_0_stop}'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{range_0_start} - {range_0_stop}'}, + ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_single_pool_options_scoped(self): shared_net_name = 'SMOKE-2' range_0_start = inc_ip(subnet, 10) - range_0_stop = inc_ip(subnet, 20) + range_0_stop = inc_ip(subnet, 20) range_router = inc_ip(subnet, 5) range_dns_1 = inc_ip(subnet, 6) @@ -320,40 +418,58 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): config = read_file(KEA4_CONF) obj = loads(config) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400 + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400 + ) # Verify shared-network options self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'option-data'], - {'name': 'domain-name', 'data': domain_name}) + obj, + ['Dhcp4', 'shared-networks', 0, 'option-data'], + {'name': 'domain-name', 'data': domain_name}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'option-data'], - {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'option-data'], + {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'option-data'], - {'name': 'routers', 'data': router}) + obj, + ['Dhcp4', 'shared-networks', 0, 'option-data'], + {'name': 'routers', 'data': router}, + ) # Verify range options self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'], - {'name': 'domain-name-servers', 'data': f'{range_dns_1}, {range_dns_2}'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'], + {'name': 'domain-name-servers', 'data': f'{range_dns_1}, {range_dns_2}'}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'], - {'name': 'routers', 'data': range_router}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'], + {'name': 'routers', 'data': range_router}, + ) # Verify pool - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], 'pool', f'{range_0_start} - {range_0_stop}') + self.verify_config_value( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + 'pool', + f'{range_0_start} - {range_0_stop}', + ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_single_pool_static_mapping(self): shared_net_name = 'SMOKE-2' @@ -375,18 +491,31 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): for client in ['client1', 'client2', 'client3']: mac = '00:50:00:00:00:{}'.format(client_base) self.cli_set(pool + ['static-mapping', client, 'mac', mac]) - self.cli_set(pool + ['static-mapping', client, 'ip-address', inc_ip(subnet, client_base)]) + self.cli_set( + pool + + ['static-mapping', client, 'ip-address', inc_ip(subnet, client_base)] + ) client_base += 1 # cannot have both mac-address and duid set with self.assertRaises(ConfigSessionError): - self.cli_set(pool + ['static-mapping', 'client1', 'duid', '00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:11']) + self.cli_set( + pool + + [ + 'static-mapping', + 'client1', + 'duid', + '00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:11', + ] + ) self.cli_commit() self.cli_delete(pool + ['static-mapping', 'client1', 'duid']) # cannot have mappings with duplicate IP addresses self.cli_set(pool + ['static-mapping', 'dupe1', 'mac', '00:50:00:00:fe:ff']) - self.cli_set(pool + ['static-mapping', 'dupe1', 'ip-address', inc_ip(subnet, 10)]) + self.cli_set( + pool + ['static-mapping', 'dupe1', 'ip-address', inc_ip(subnet, 10)] + ) with self.assertRaises(ConfigSessionError): self.cli_commit() # Should allow disabled duplicate @@ -396,17 +525,38 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): # cannot have mappings with duplicate MAC addresses self.cli_set(pool + ['static-mapping', 'dupe2', 'mac', '00:50:00:00:00:10']) - self.cli_set(pool + ['static-mapping', 'dupe2', 'ip-address', inc_ip(subnet, 120)]) + self.cli_set( + pool + ['static-mapping', 'dupe2', 'ip-address', inc_ip(subnet, 120)] + ) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(pool + ['static-mapping', 'dupe2']) - # cannot have mappings with duplicate MAC addresses - self.cli_set(pool + ['static-mapping', 'dupe3', 'duid', '00:01:02:03:04:05:06:07:aa:aa:aa:aa:aa:01']) - self.cli_set(pool + ['static-mapping', 'dupe3', 'ip-address', inc_ip(subnet, 121)]) - self.cli_set(pool + ['static-mapping', 'dupe4', 'duid', '00:01:02:03:04:05:06:07:aa:aa:aa:aa:aa:01']) - self.cli_set(pool + ['static-mapping', 'dupe4', 'ip-address', inc_ip(subnet, 121)]) + self.cli_set( + pool + + [ + 'static-mapping', + 'dupe3', + 'duid', + '00:01:02:03:04:05:06:07:aa:aa:aa:aa:aa:01', + ] + ) + self.cli_set( + pool + ['static-mapping', 'dupe3', 'ip-address', inc_ip(subnet, 121)] + ) + self.cli_set( + pool + + [ + 'static-mapping', + 'dupe4', + 'duid', + '00:01:02:03:04:05:06:07:aa:aa:aa:aa:aa:01', + ] + ) + self.cli_set( + pool + ['static-mapping', 'dupe4', 'ip-address', inc_ip(subnet, 121)] + ) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(pool + ['static-mapping', 'dupe3']) @@ -418,25 +568,38 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): config = read_file(KEA4_CONF) obj = loads(config) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1 + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400 + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400 + ) # Verify options self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'domain-name', 'data': domain_name}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-name', 'data': domain_name}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'routers', 'data': router}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}, + ) client_base = 10 for client in ['client1', 'client2', 'client3']: @@ -444,14 +607,15 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ip = inc_ip(subnet, client_base) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'reservations'], - {'hostname': client, 'hw-address': mac, 'ip-address': ip}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'reservations'], + {'hostname': client, 'hw-address': mac, 'ip-address': ip}, + ) client_base += 1 # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_multiple_pools(self): lease_time = '14400' @@ -463,11 +627,16 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): dns_1 = inc_ip(subnet, 2) range_0_start = inc_ip(subnet, 10) - range_0_stop = inc_ip(subnet, 20) + range_0_stop = inc_ip(subnet, 20) range_1_start = inc_ip(subnet, 30) - range_1_stop = inc_ip(subnet, 40) - - pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] + range_1_stop = inc_ip(subnet, 40) + + pool = base_path + [ + 'shared-network-name', + shared_net_name, + 'subnet', + subnet, + ] self.cli_set(pool + ['subnet-id', str(int(network) + 1)]) # we use the first subnet IP address as default gateway self.cli_set(pool + ['option', 'default-router', router]) @@ -484,7 +653,15 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): for client in ['client1', 'client2', 'client3', 'client4']: mac = '02:50:00:00:00:{}'.format(client_base) self.cli_set(pool + ['static-mapping', client, 'mac', mac]) - self.cli_set(pool + ['static-mapping', client, 'ip-address', inc_ip(subnet, client_base)]) + self.cli_set( + pool + + [ + 'static-mapping', + client, + 'ip-address', + inc_ip(subnet, client_base), + ] + ) client_base += 1 # commit changes @@ -500,37 +677,64 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): dns_1 = inc_ip(subnet, 2) range_0_start = inc_ip(subnet, 10) - range_0_stop = inc_ip(subnet, 20) + range_0_stop = inc_ip(subnet, 20) range_1_start = inc_ip(subnet, 30) - range_1_stop = inc_ip(subnet, 40) + range_1_stop = inc_ip(subnet, 40) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'subnet', subnet) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'id', int(network) + 1) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'valid-lifetime', int(lease_time)) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'max-valid-lifetime', int(lease_time)) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name + ) + self.verify_config_value( + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4'], + 'subnet', + subnet, + ) + self.verify_config_value( + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4'], + 'id', + int(network) + 1, + ) + self.verify_config_value( + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4'], + 'valid-lifetime', + int(lease_time), + ) + self.verify_config_value( + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4'], + 'max-valid-lifetime', + int(lease_time), + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'], - {'name': 'domain-name', 'data': domain_name}) + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'], + {'name': 'domain-name', 'data': domain_name}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'], - {'name': 'domain-name-servers', 'data': dns_1}) + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'], + {'name': 'domain-name-servers', 'data': dns_1}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'], - {'name': 'routers', 'data': router}) + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'], - {'pool': f'{range_0_start} - {range_0_stop}'}) + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'], + {'pool': f'{range_0_start} - {range_0_stop}'}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'], - {'pool': f'{range_1_start} - {range_1_stop}'}) + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'], + {'pool': f'{range_1_start} - {range_1_stop}'}, + ) client_base = 60 for client in ['client1', 'client2', 'client3', 'client4']: @@ -538,25 +742,34 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ip = inc_ip(subnet, client_base) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'reservations'], - {'hostname': client, 'hw-address': mac, 'ip-address': ip}) + obj, + [ + 'Dhcp4', + 'shared-networks', + int(network), + 'subnet4', + 0, + 'reservations', + ], + {'hostname': client, 'hw-address': mac, 'ip-address': ip}, + ) client_base += 1 # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_exclude_not_in_range(self): # T3180: verify else path when slicing DHCP ranges and exclude address # is not part of the DHCP range range_0_start = inc_ip(subnet, 10) - range_0_stop = inc_ip(subnet, 20) + range_0_stop = inc_ip(subnet, 20) pool = base_path + ['shared-network-name', 'EXCLUDE-TEST', 'subnet', subnet] self.cli_set(pool + ['subnet-id', '1']) self.cli_set(pool + ['option', 'default-router', router]) self.cli_set(pool + ['exclude', router]) + self.cli_set(pool + ['range', '0', 'option', 'default-router', router]) self.cli_set(pool + ['range', '0', 'start', range_0_start]) self.cli_set(pool + ['range', '0', 'stop', range_0_stop]) @@ -566,33 +779,42 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): config = read_file(KEA4_CONF) obj = loads(config) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST') - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST' + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet + ) + + pool_obj = { + 'pool': f'{range_0_start} - {range_0_stop}', + 'option-data': [{'name': 'routers', 'data': router}], + } # Verify options self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'routers', 'data': router}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}, + ) # Verify pools self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], - {'pool': f'{range_0_start} - {range_0_stop}'}) + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], pool_obj + ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_exclude_in_range(self): # T3180: verify else path when slicing DHCP ranges and exclude address # is not part of the DHCP range range_0_start = inc_ip(subnet, 10) - range_0_stop = inc_ip(subnet, 100) + range_0_stop = inc_ip(subnet, 100) # the DHCP exclude addresse is blanked out of the range which is done # by slicing one range into two ranges - exclude_addr = inc_ip(range_0_start, 20) + exclude_addr = inc_ip(range_0_start, 20) range_0_stop_excl = dec_ip(exclude_addr, 1) range_0_start_excl = inc_ip(exclude_addr, 1) @@ -600,6 +822,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.cli_set(pool + ['subnet-id', '1']) self.cli_set(pool + ['option', 'default-router', router]) self.cli_set(pool + ['exclude', exclude_addr]) + self.cli_set(pool + ['range', '0', 'option', 'default-router', router]) self.cli_set(pool + ['range', '0', 'start', range_0_start]) self.cli_set(pool + ['range', '0', 'stop', range_0_stop]) @@ -609,27 +832,42 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): config = read_file(KEA4_CONF) obj = loads(config) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST-2') - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST-2' + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet + ) + + pool_obj = { + 'pool': f'{range_0_start} - {range_0_stop_excl}', + 'option-data': [{'name': 'routers', 'data': router}], + } + + pool_exclude_obj = { + 'pool': f'{range_0_start_excl} - {range_0_stop}', + 'option-data': [{'name': 'routers', 'data': router}], + } # Verify options self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'routers', 'data': router}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], - {'pool': f'{range_0_start} - {range_0_stop_excl}'}) + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], pool_obj + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], - {'pool': f'{range_0_start_excl} - {range_0_stop}'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + pool_exclude_obj, + ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_relay_server(self): # Listen on specific address and return DHCP leases from a non @@ -640,7 +878,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): relay_router = inc_ip(relay_subnet, 1) range_0_start = '10.0.1.0' - range_0_stop = '10.0.250.255' + range_0_stop = '10.0.250.255' pool = base_path + ['shared-network-name', 'RELAY', 'subnet', relay_subnet] self.cli_set(pool + ['subnet-id', '1']) @@ -654,31 +892,37 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): config = read_file(KEA4_CONF) obj = loads(config) - self.verify_config_value(obj, ['Dhcp4', 'interfaces-config'], 'interfaces', [f'{interface}/{router}']) + self.verify_config_value( + obj, ['Dhcp4', 'interfaces-config'], 'interfaces', [f'{interface}/{router}'] + ) self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'RELAY') - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', relay_subnet) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', relay_subnet + ) # Verify options self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'routers', 'data': relay_router}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': relay_router}, + ) # Verify pools self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], - {'pool': f'{range_0_start} - {range_0_stop}'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{range_0_start} - {range_0_stop}'}, + ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_high_availability(self): shared_net_name = 'FAILOVER' failover_name = 'VyOS-Failover' range_0_start = inc_ip(subnet, 10) - range_0_stop = inc_ip(subnet, 20) + range_0_stop = inc_ip(subnet, 20) pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] self.cli_set(pool + ['subnet-id', '1']) @@ -695,7 +939,9 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): failover_local = router failover_remote = inc_ip(router, 1) - self.cli_set(base_path + ['high-availability', 'source-address', failover_local]) + self.cli_set( + base_path + ['high-availability', 'source-address', failover_local] + ) self.cli_set(base_path + ['high-availability', 'name', failover_name]) self.cli_set(base_path + ['high-availability', 'remote', failover_remote]) self.cli_set(base_path + ['high-availability', 'status', 'primary']) @@ -708,43 +954,78 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): obj = loads(config) # Verify failover - self.verify_config_value(obj, ['Dhcp4', 'control-socket'], 'socket-name', KEA4_CTRL) + self.verify_config_value( + obj, ['Dhcp4', 'control-socket'], 'socket-name', KEA4_CTRL + ) self.verify_config_object( obj, - ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'], - {'name': os.uname()[1], 'url': f'http://{failover_local}:647/', 'role': 'primary', 'auto-failover': True}) + [ + 'Dhcp4', + 'hooks-libraries', + 0, + 'parameters', + 'high-availability', + 0, + 'peers', + ], + { + 'name': os.uname()[1], + 'url': f'http://{failover_local}:647/', + 'role': 'primary', + 'auto-failover': True, + }, + ) self.verify_config_object( obj, - ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'], - {'name': failover_name, 'url': f'http://{failover_remote}:647/', 'role': 'secondary', 'auto-failover': True}) - - self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) + [ + 'Dhcp4', + 'hooks-libraries', + 0, + 'parameters', + 'high-availability', + 0, + 'peers', + ], + { + 'name': failover_name, + 'url': f'http://{failover_remote}:647/', + 'role': 'secondary', + 'auto-failover': True, + }, + ) + + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet + ) # Verify options self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'routers', 'data': router}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}, + ) # Verify pools self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], - {'pool': f'{range_0_start} - {range_0_stop}'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{range_0_start} - {range_0_stop}'}, + ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - self.assertTrue(process_named_running(CTRL_PROCESS_NAME)) + self.verify_service_running() def test_dhcp_high_availability_standby(self): shared_net_name = 'FAILOVER' failover_name = 'VyOS-Failover' range_0_start = inc_ip(subnet, 10) - range_0_stop = inc_ip(subnet, 20) + range_0_stop = inc_ip(subnet, 20) pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] self.cli_set(pool + ['subnet-id', '1']) @@ -757,7 +1038,9 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): failover_local = router failover_remote = inc_ip(router, 1) - self.cli_set(base_path + ['high-availability', 'source-address', failover_local]) + self.cli_set( + base_path + ['high-availability', 'source-address', failover_local] + ) self.cli_set(base_path + ['high-availability', 'name', failover_name]) self.cli_set(base_path + ['high-availability', 'remote', failover_remote]) self.cli_set(base_path + ['high-availability', 'status', 'secondary']) @@ -770,61 +1053,383 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): obj = loads(config) # Verify failover - self.verify_config_value(obj, ['Dhcp4', 'control-socket'], 'socket-name', KEA4_CTRL) + self.verify_config_value( + obj, ['Dhcp4', 'control-socket'], 'socket-name', KEA4_CTRL + ) self.verify_config_object( obj, - ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'], - {'name': os.uname()[1], 'url': f'http://{failover_local}:647/', 'role': 'standby', 'auto-failover': True}) + [ + 'Dhcp4', + 'hooks-libraries', + 0, + 'parameters', + 'high-availability', + 0, + 'peers', + ], + { + 'name': os.uname()[1], + 'url': f'http://{failover_local}:647/', + 'role': 'standby', + 'auto-failover': True, + }, + ) self.verify_config_object( obj, - ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'], - {'name': failover_name, 'url': f'http://{failover_remote}:647/', 'role': 'primary', 'auto-failover': True}) + [ + 'Dhcp4', + 'hooks-libraries', + 0, + 'parameters', + 'high-availability', + 0, + 'peers', + ], + { + 'name': failover_name, + 'url': f'http://{failover_remote}:647/', + 'role': 'primary', + 'auto-failover': True, + }, + ) + + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet + ) + + # Verify options + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}, + ) + + # Verify pools + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{range_0_start} - {range_0_stop}'}, + ) + + # Check for running process + self.verify_service_running() + + def test_dhcp_dynamic_dns_update(self): + shared_net_name = 'SMOKE-1DDNS' + + range_0_start = inc_ip(subnet, 10) + range_0_stop = inc_ip(subnet, 20) + + self.cli_set(base_path + ['listen-interface', interface]) + + ddns = base_path + ['dynamic-dns-update'] + + self.cli_set(ddns + ['send-updates', 'enable']) + self.cli_set(ddns + ['conflict-resolution', 'enable']) + self.cli_set(ddns + ['override-no-update', 'enable']) + self.cli_set(ddns + ['override-client-update', 'enable']) + self.cli_set(ddns + ['replace-client-name', 'always']) + self.cli_set(ddns + ['update-on-renew', 'enable']) + + self.cli_set(ddns + ['tsig-key', 'domain-lan-updates', 'algorithm', 'sha256']) + self.cli_set(ddns + ['tsig-key', 'domain-lan-updates', 'secret', 'SXQncyBXZWRuZXNkYXkgbWFoIGR1ZGVzIQ==']) + self.cli_set(ddns + ['tsig-key', 'reverse-0-168-192', 'algorithm', 'sha256']) + self.cli_set(ddns + ['tsig-key', 'reverse-0-168-192', 'secret', 'VGhhbmsgR29kIGl0J3MgRnJpZGF5IQ==']) + self.cli_set(ddns + ['forward-domain', 'domain.lan', 'dns-server', '1', 'address', '192.168.0.1']) + self.cli_set(ddns + ['forward-domain', 'domain.lan', 'dns-server', '2', 'address', '100.100.0.1']) + self.cli_set(ddns + ['forward-domain', 'domain.lan', 'key-name', 'domain-lan-updates']) + self.cli_set(ddns + ['reverse-domain', '0.168.192.in-addr.arpa', 'dns-server', '1', 'address', '192.168.0.1']) + self.cli_set(ddns + ['reverse-domain', '0.168.192.in-addr.arpa', 'dns-server', '1', 'port', '1053']) + self.cli_set(ddns + ['reverse-domain', '0.168.192.in-addr.arpa', 'dns-server', '2', 'address', '100.100.0.1']) + self.cli_set(ddns + ['reverse-domain', '0.168.192.in-addr.arpa', 'dns-server', '2', 'port', '1153']) + self.cli_set(ddns + ['reverse-domain', '0.168.192.in-addr.arpa', 'key-name', 'reverse-0-168-192']) + + shared = base_path + ['shared-network-name', shared_net_name] + + self.cli_set(shared + ['dynamic-dns-update', 'send-updates', 'enable']) + self.cli_set(shared + ['dynamic-dns-update', 'conflict-resolution', 'enable']) + self.cli_set(shared + ['dynamic-dns-update', 'ttl-percent', '75']) + pool = shared + [ 'subnet', subnet] + + self.cli_set(pool + ['subnet-id', '1']) + + self.cli_set(pool + ['range', '0', 'start', range_0_start]) + self.cli_set(pool + ['range', '0', 'stop', range_0_stop]) + + self.cli_set(pool + ['dynamic-dns-update', 'send-updates', 'enable']) + self.cli_set(pool + ['dynamic-dns-update', 'generated-prefix', 'myfunnyprefix']) + self.cli_set(pool + ['dynamic-dns-update', 'qualifying-suffix', 'suffix.lan']) + self.cli_set(pool + ['dynamic-dns-update', 'hostname-char-set', 'xXyYzZ']) + self.cli_set(pool + ['dynamic-dns-update', 'hostname-char-replacement', '_xXx_']) + + self.cli_commit() + + config = read_file(KEA4_CONF) + d2_config = read_file(KEA4_D2_CONF) + + obj = loads(config) + d2_obj = loads(d2_config) + + # Verify global DDNS parameters in the main config file + self.verify_config_value( + obj, + ['Dhcp4'], 'dhcp-ddns', + {'enable-updates': True, 'server-ip': '127.0.0.1', 'server-port': 53001, 'sender-ip': '', 'sender-port': 0, + 'max-queue-size': 1024, 'ncr-protocol': 'UDP', 'ncr-format': 'JSON'}) + + self.verify_config_value(obj, ['Dhcp4'], 'ddns-send-updates', True) + self.verify_config_value(obj, ['Dhcp4'], 'ddns-use-conflict-resolution', True) + self.verify_config_value(obj, ['Dhcp4'], 'ddns-override-no-update', True) + self.verify_config_value(obj, ['Dhcp4'], 'ddns-override-client-update', True) + self.verify_config_value(obj, ['Dhcp4'], 'ddns-replace-client-name', 'always') + self.verify_config_value(obj, ['Dhcp4'], 'ddns-update-on-renew', True) + + # Verify scoped DDNS parameters in the main config file self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'ddns-send-updates', True) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'ddns-use-conflict-resolution', True) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'ddns-ttl-percent', 0.75) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'ddns-send-updates', True) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'ddns-generated-prefix', 'myfunnyprefix') + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'ddns-qualifying-suffix', 'suffix.lan') + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'hostname-char-set', 'xXyYzZ') + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'hostname-char-replacement', '_xXx_') - # Verify options + # Verify keys and domains configuration in the D2 config self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'routers', 'data': router}) + d2_obj, + ['DhcpDdns', 'tsig-keys'], + {'name': 'domain-lan-updates', 'algorithm': 'HMAC-SHA256', 'secret': 'SXQncyBXZWRuZXNkYXkgbWFoIGR1ZGVzIQ=='} + ) + self.verify_config_object( + d2_obj, + ['DhcpDdns', 'tsig-keys'], + {'name': 'reverse-0-168-192', 'algorithm': 'HMAC-SHA256', 'secret': 'VGhhbmsgR29kIGl0J3MgRnJpZGF5IQ=='} + ) - # Verify pools + self.verify_config_value(d2_obj, ['DhcpDdns', 'forward-ddns', 'ddns-domains', 0], 'name', 'domain.lan') + self.verify_config_value(d2_obj, ['DhcpDdns', 'forward-ddns', 'ddns-domains', 0], 'key-name', 'domain-lan-updates') self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], - {'pool': f'{range_0_start} - {range_0_stop}'}) + d2_obj, + ['DhcpDdns', 'forward-ddns', 'ddns-domains', 0, 'dns-servers'], + {'ip-address': '192.168.0.1'} + ) + self.verify_config_object( + d2_obj, + ['DhcpDdns', 'forward-ddns', 'ddns-domains', 0, 'dns-servers'], + {'ip-address': '100.100.0.1'} + ) + + self.verify_config_value(d2_obj, ['DhcpDdns', 'reverse-ddns', 'ddns-domains', 0], 'name', '0.168.192.in-addr.arpa') + self.verify_config_value(d2_obj, ['DhcpDdns', 'reverse-ddns', 'ddns-domains', 0], 'key-name', 'reverse-0-168-192') + self.verify_config_object( + d2_obj, + ['DhcpDdns', 'reverse-ddns', 'ddns-domains', 0, 'dns-servers'], + {'ip-address': '192.168.0.1', 'port': 1053} + ) + self.verify_config_object( + d2_obj, + ['DhcpDdns', 'reverse-ddns', 'ddns-domains', 0, 'dns-servers'], + {'ip-address': '100.100.0.1', 'port': 1153} + ) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) - self.assertTrue(process_named_running(CTRL_PROCESS_NAME)) + self.assertTrue(process_named_running(D2_PROCESS_NAME)) def test_dhcp_on_interface_with_vrf(self): self.cli_set(['interfaces', 'ethernet', 'eth1', 'address', '10.1.1.1/30']) self.cli_set(['interfaces', 'ethernet', 'eth1', 'vrf', 'SMOKE-DHCP']) - self.cli_set(['protocols', 'static', 'route', '10.1.10.0/24', 'interface', 'eth1', 'vrf', 'SMOKE-DHCP']) - self.cli_set(['vrf', 'name', 'SMOKE-DHCP', 'protocols', 'static', 'route', '10.1.10.0/24', 'next-hop', '10.1.1.2']) + self.cli_set( + [ + 'protocols', + 'static', + 'route', + '10.1.10.0/24', + 'interface', + 'eth1', + 'vrf', + 'SMOKE-DHCP', + ] + ) + self.cli_set( + [ + 'vrf', + 'name', + 'SMOKE-DHCP', + 'protocols', + 'static', + 'route', + '10.1.10.0/24', + 'next-hop', + '10.1.1.2', + ] + ) self.cli_set(['vrf', 'name', 'SMOKE-DHCP', 'table', '1000']) - self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'subnet-id', '1']) - self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'option', 'default-router', '10.1.10.1']) - self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'option', 'name-server', '1.1.1.1']) - self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'range', '1', 'start', '10.1.10.10']) - self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'range', '1', 'stop', '10.1.10.20']) + self.cli_set( + base_path + + [ + 'shared-network-name', + 'SMOKE-DHCP-NETWORK', + 'subnet', + '10.1.10.0/24', + 'subnet-id', + '1', + ] + ) + self.cli_set( + base_path + + [ + 'shared-network-name', + 'SMOKE-DHCP-NETWORK', + 'subnet', + '10.1.10.0/24', + 'option', + 'default-router', + '10.1.10.1', + ] + ) + self.cli_set( + base_path + + [ + 'shared-network-name', + 'SMOKE-DHCP-NETWORK', + 'subnet', + '10.1.10.0/24', + 'option', + 'name-server', + '1.1.1.1', + ] + ) + self.cli_set( + base_path + + [ + 'shared-network-name', + 'SMOKE-DHCP-NETWORK', + 'subnet', + '10.1.10.0/24', + 'range', + '1', + 'start', + '10.1.10.10', + ] + ) + self.cli_set( + base_path + + [ + 'shared-network-name', + 'SMOKE-DHCP-NETWORK', + 'subnet', + '10.1.10.0/24', + 'range', + '1', + 'stop', + '10.1.10.20', + ] + ) self.cli_set(base_path + ['listen-address', '10.1.1.1']) self.cli_commit() config = read_file(KEA4_CONF) obj = loads(config) - self.verify_config_value(obj, ['Dhcp4', 'interfaces-config'], 'interfaces', ['eth1/10.1.1.1']) + self.verify_config_value( + obj, ['Dhcp4', 'interfaces-config'], 'interfaces', ['eth1/10.1.1.1'] + ) self.cli_delete(['interfaces', 'ethernet', 'eth1', 'vrf', 'SMOKE-DHCP']) - self.cli_delete(['protocols', 'static', 'route', '10.1.10.0/24', 'interface', 'eth1', 'vrf']) + self.cli_delete( + ['protocols', 'static', 'route', '10.1.10.0/24', 'interface', 'eth1', 'vrf'] + ) self.cli_delete(['vrf', 'name', 'SMOKE-DHCP']) self.cli_commit() + def test_dhcp_hostsd_lease_sync(self): + shared_net_name = 'SMOKE-LEASE-SYNC' + domain_name = 'sync.private' + + client_range = range(1, 4) + subnet_range_start = inc_ip(subnet, 10) + subnet_range_stop = inc_ip(subnet, 20) + + def internal_cleanup(): + for seq in client_range: + ip_addr = inc_ip(subnet, seq) + kea_delete_lease(4, ip_addr) + cmd( + f'{HOSTSD_CLIENT} --delete-hosts --tag dhcp-server-{ip_addr} --apply' + ) + + self.addClassCleanup(internal_cleanup) + + pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] + self.cli_set(pool + ['subnet-id', '1']) + self.cli_set(pool + ['option', 'domain-name', domain_name]) + self.cli_set(pool + ['range', '0', 'start', subnet_range_start]) + self.cli_set(pool + ['range', '0', 'stop', subnet_range_stop]) + + # commit changes + self.cli_commit() + + config = read_file(KEA4_CONF) + obj = loads(config) + + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1 + ) + + # Verify options + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-name', 'data': domain_name}, + ) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{subnet_range_start} - {subnet_range_stop}'}, + ) + + # Check for running process + self.verify_service_running() + + # All up and running, now test vyos-hostsd store + + # 1. Inject leases into kea + for seq in client_range: + client = f'client{seq}' + mac = f'00:50:00:00:00:{seq:02}' + ip = inc_ip(subnet, seq) + kea_add_lease(4, ip, host_name=client, mac_address=mac) + + # 2. Verify that leases are not available in vyos-hostsd + tag_regex = re.escape(f'dhcp-server-{subnet.rsplit(".", 1)[0]}') + host_json = cmd(f'{HOSTSD_CLIENT} --get-hosts {tag_regex}') + self.assertFalse(host_json.strip('{}')) + + # 3. Restart the service to trigger vyos-hostsd sync and wait for it to start + self.assertTrue(process_named_running(PROCESS_NAME, timeout=30)) + + # 4. Verify that leases are synced and available in vyos-hostsd + tag_regex = re.escape(f'dhcp-server-{subnet.rsplit(".", 1)[0]}') + host_json = cmd(f'{HOSTSD_CLIENT} --get-hosts {tag_regex}') + self.assertTrue(host_json) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_dhcpv6-server.py b/smoketest/scripts/cli/test_service_dhcpv6-server.py index 6ecf6c1cf..6535ca72d 100755 --- a/smoketest/scripts/cli/test_service_dhcpv6-server.py +++ b/smoketest/scripts/cli/test_service_dhcpv6-server.py @@ -108,6 +108,7 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): self.cli_set(pool + ['lease-time', 'default', lease_time]) self.cli_set(pool + ['lease-time', 'maximum', max_lease_time]) self.cli_set(pool + ['lease-time', 'minimum', min_lease_time]) + self.cli_set(pool + ['option', 'capwap-controller', dns_1]) self.cli_set(pool + ['option', 'name-server', dns_1]) self.cli_set(pool + ['option', 'name-server', dns_2]) self.cli_set(pool + ['option', 'name-server', dns_2]) @@ -157,6 +158,10 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): self.verify_config_object( obj, ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], + {'name': 'capwap-ac-v6', 'data': dns_1}) + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], {'name': 'dns-servers', 'data': f'{dns_1}, {dns_2}'}) self.verify_config_object( obj, diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index c39d4467a..522102e67 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright (C) 2019-2025 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 @@ -14,23 +14,24 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os import unittest import tempfile from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError +from vyos.utils.file import read_file from vyos.utils.process import cmd -from vyos.utils.process import process_running +from vyos.utils.process import process_named_running +from vyos.xml_ref import default_value DDCLIENT_SYSTEMD_UNIT = '/run/systemd/system/ddclient.service.d/override.conf' DDCLIENT_CONF = '/run/ddclient/ddclient.conf' -DDCLIENT_PID = '/run/ddclient/ddclient.pid' DDCLIENT_PNAME = 'ddclient' base_path = ['service', 'dns', 'dynamic'] name_path = base_path + ['name'] +default_interval = default_value(base_path + ['interval']) server = 'ddns.vyos.io' hostname = 'test.ddns.vyos.io' zone = 'vyos.io' @@ -40,20 +41,24 @@ ttl = '300' interface = 'eth0' class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): - def setUp(self): - # Always start with a clean CLI instance - self.cli_delete(base_path) + @classmethod + def setUpClass(cls): + super(TestServiceDDNS, cls).setUpClass() + + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) def tearDown(self): # Check for running process - self.assertTrue(process_running(DDCLIENT_PID)) + self.assertTrue(process_named_running(DDCLIENT_PNAME, timeout=10)) # Delete DDNS configuration self.cli_delete(base_path) self.cli_commit() - # PID file must no londer exist after process exited - self.assertFalse(os.path.exists(DDCLIENT_PID)) + # Check for process not running anymore + self.assertFalse(process_named_running(DDCLIENT_PNAME)) # IPv4 standard DDNS service configuration def test_01_dyndns_service_standard(self): @@ -93,12 +98,14 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # Check the generating config parameters ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}') - # default value 300 seconds - self.assertIn(f'daemon=300', ddclient_conf) self.assertIn(f'usev4=ifv4', ddclient_conf) self.assertIn(f'ifv4={interface}', ddclient_conf) self.assertIn(f'password=\'{password}\'', ddclient_conf) + # Check default interval of 300 seconds + systemd_override = read_file(DDCLIENT_SYSTEMD_UNIT) + self.assertIn(f'--daemon {default_interval}', systemd_override) + for opt in details.keys(): if opt == 'username': login = details[opt] @@ -138,7 +145,6 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # Check the generating config parameters ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}') - self.assertIn(f'daemon={interval}', ddclient_conf) self.assertIn(f'usev6=ifv6', ddclient_conf) self.assertIn(f'ifv6={interface}', ddclient_conf) self.assertIn(f'protocol={proto}', ddclient_conf) @@ -148,6 +154,10 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.assertIn(f'min-interval={wait_time}', ddclient_conf) self.assertIn(f'max-interval={expiry_time_good}', ddclient_conf) + # default value 300 seconds + systemd_override = read_file(DDCLIENT_SYSTEMD_UNIT) + self.assertIn(f'--daemon {interval}', systemd_override) + # IPv4+IPv6 dual DDNS service configuration def test_03_dyndns_service_dual_stack(self): services = {'cloudflare': {'protocol': 'cloudflare', 'zone': zone}, @@ -337,9 +347,10 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Check for process in VRF - systemd_override = cmd(f'cat {DDCLIENT_SYSTEMD_UNIT}') - self.assertIn(f'ExecStart=ip vrf exec {vrf_name} /usr/bin/ddclient -file {DDCLIENT_CONF}', - systemd_override) + systemd_override = read_file(DDCLIENT_SYSTEMD_UNIT) + self.assertIn(f'ExecStart=ip vrf exec {vrf_name} /usr/bin/ddclient ' \ + f'--file {DDCLIENT_CONF} --cache {DDCLIENT_CONF.replace("conf", "cache")} ' \ + f'--foreground --daemon {default_interval}', systemd_override) # Check for process in VRF proc = cmd(f'ip vrf pids {vrf_name}') diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py index 8a6386e4f..b4fe35d81 100755 --- a/smoketest/scripts/cli/test_service_https.py +++ b/smoketest/scripts/cli/test_service_https.py @@ -16,6 +16,7 @@ import unittest import json +import psutil from requests import request from urllib3.exceptions import InsecureRequestWarning @@ -89,6 +90,7 @@ server { PROCESS_NAME = 'nginx' + class TestHTTPSService(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): @@ -112,6 +114,29 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): # Check for stopped process self.assertFalse(process_named_running(PROCESS_NAME)) + def test_listen_address(self): + test_prefix = ['192.0.2.1/26', '2001:db8:1::ffff/64'] + test_addr = [ i.split('/')[0] for i in test_prefix ] + for i, addr in enumerate(test_prefix): + self.cli_set(['interfaces', 'dummy', f'dum{i}', 'address', addr]) + + key = 'MySuperSecretVyOS' + self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key]) + # commit base config first, for testing update of listen-address + self.cli_commit() + + for addr in test_addr: + self.cli_set(base_path + ['listen-address', addr]) + self.cli_commit() + + res = set() + t = psutil.net_connections(kind="tcp") + for c in t: + if c.laddr.port == 443: + res.add(c.laddr.ip) + + self.assertEqual(res, set(test_addr)) + def test_certificate(self): cert_name = 'test_https' dh_name = 'dh-test' @@ -120,19 +145,29 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): # verify() - certificates do not exist (yet) with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(pki_base + ['certificate', cert_name, 'certificate', cert_data.replace('\n','')]) - self.cli_set(pki_base + ['certificate', cert_name, 'private', 'key', key_data.replace('\n','')]) + self.cli_set( + pki_base + + ['certificate', cert_name, 'certificate', cert_data.replace('\n', '')] + ) + self.cli_set( + pki_base + + ['certificate', cert_name, 'private', 'key', key_data.replace('\n', '')] + ) self.cli_set(base_path + ['certificates', 'dh-params', dh_name]) # verify() - dh-params do not exist (yet) with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(pki_base + ['dh', dh_name, 'parameters', dh_1024.replace('\n','')]) + self.cli_set( + pki_base + ['dh', dh_name, 'parameters', dh_1024.replace('\n', '')] + ) # verify() - dh-param minimum length is 2048 bit with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(pki_base + ['dh', dh_name, 'parameters', dh_2048.replace('\n','')]) + self.cli_set( + pki_base + ['dh', dh_name, 'parameters', dh_2048.replace('\n', '')] + ) self.cli_commit() self.assertTrue(process_named_running(PROCESS_NAME)) @@ -154,13 +189,15 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): key = 'MySuperSecretVyOS' self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key]) + self.cli_set(base_path + ['api', 'rest']) + self.cli_set(base_path + ['listen-address', address]) self.cli_commit() nginx_config = read_file('/etc/nginx/sites-enabled/default') self.assertIn(f'listen {address}:{port} ssl;', nginx_config) - self.assertIn(f'ssl_protocols TLSv1.2 TLSv1.3;', nginx_config) # default + self.assertIn('ssl_protocols TLSv1.2 TLSv1.3;', nginx_config) # default url = f'https://{address}/retrieve' payload = {'data': '{"op": "showConfig", "path": []}', 'key': f'{key}'} @@ -180,11 +217,16 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): self.assertEqual(r.status_code, 401) # Check path config - payload = {'data': '{"op": "showConfig", "path": ["system", "login"]}', 'key': f'{key}'} + payload = { + 'data': '{"op": "showConfig", "path": ["system", "login"]}', + 'key': f'{key}', + } r = request('POST', url, verify=False, headers=headers, data=payload) response = r.json() vyos_user_exists = 'vyos' in response.get('data', {}).get('user', {}) - self.assertTrue(vyos_user_exists, "The 'vyos' user does not exist in the response.") + self.assertTrue( + vyos_user_exists, "The 'vyos' user does not exist in the response." + ) # GraphQL auth test: a missing key will return status code 400, as # 'key' is a non-nullable field in the schema; an incorrect key is @@ -208,7 +250,13 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): }} """ - r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query_valid_key}) + r = request( + 'POST', + graphql_url, + verify=False, + headers=headers, + json={'query': query_valid_key}, + ) success = r.json()['data']['SystemStatus']['success'] self.assertTrue(success) @@ -224,7 +272,13 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): } """ - r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query_invalid_key}) + r = request( + 'POST', + graphql_url, + verify=False, + headers=headers, + json={'query': query_invalid_key}, + ) success = r.json()['data']['SystemStatus']['success'] self.assertFalse(success) @@ -240,7 +294,13 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): } """ - r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query_no_key}) + r = request( + 'POST', + graphql_url, + verify=False, + headers=headers, + json={'query': query_no_key}, + ) success = r.json()['data']['SystemStatus']['success'] self.assertFalse(success) @@ -261,7 +321,9 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): } } """ - r = request('POST', graphql_url, verify=False, headers=headers, json={'query': mutation}) + r = request( + 'POST', graphql_url, verify=False, headers=headers, json={'query': mutation} + ) token = r.json()['data']['AuthToken']['data']['result']['token'] @@ -284,7 +346,9 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): } """ - r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query}) + r = request( + 'POST', graphql_url, verify=False, headers=headers, json={'query': query} + ) success = r.json()['data']['ShowVersion']['success'] self.assertTrue(success) @@ -304,6 +368,7 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): self.assertEqual(r.status_code, 503) self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key]) + self.cli_set(base_path + ['api', 'rest']) self.cli_commit() sleep(2) @@ -326,6 +391,7 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): headers = {} self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key]) + self.cli_set(base_path + ['api', 'rest']) self.cli_commit() payload = { @@ -343,6 +409,7 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): headers = {} self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key]) + self.cli_set(base_path + ['api', 'rest']) self.cli_commit() payload = { @@ -362,17 +429,18 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): conf_address = '192.0.2.44/32' self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key]) + self.cli_set(base_path + ['api', 'rest']) self.cli_commit() payload_path = [ - "interfaces", - "dummy", - f"{conf_interface}", - "address", - f"{conf_address}", + 'interfaces', + 'dummy', + f'{conf_interface}', + 'address', + f'{conf_address}', ] - payload = {'data': json.dumps({"op": "set", "path": payload_path}), 'key': key} + payload = {'data': json.dumps({'op': 'set', 'path': payload_path}), 'key': key} r = request('POST', url, verify=False, headers=headers, data=payload) self.assertEqual(r.status_code, 200) @@ -385,6 +453,7 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): headers = {} self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key]) + self.cli_set(base_path + ['api', 'rest']) self.cli_commit() payload = { @@ -402,6 +471,7 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): headers = {} self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key]) + self.cli_set(base_path + ['api', 'rest']) self.cli_commit() payload = { @@ -419,6 +489,7 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): headers = {} self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key]) + self.cli_set(base_path + ['api', 'rest']) self.cli_commit() payload = { @@ -462,6 +533,7 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): headers = {} self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key]) + self.cli_set(base_path + ['api', 'rest']) self.cli_commit() # load config via HTTP requires nginx config @@ -498,5 +570,6 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): call(f'sudo rm -f {nginx_tmp_site}') call('sudo systemctl reload nginx') + if __name__ == '__main__': unittest.main(verbosity=5) diff --git a/smoketest/scripts/cli/test_service_ids_ddos-protection.py b/smoketest/scripts/cli/test_service_ids_ddos-protection.py deleted file mode 100755 index 91b056eea..000000000 --- a/smoketest/scripts/cli/test_service_ids_ddos-protection.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2022 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import os -import unittest - -from base_vyostest_shim import VyOSUnitTestSHIM - -from vyos.configsession import ConfigSessionError -from vyos.utils.process import process_named_running -from vyos.utils.file import read_file - -PROCESS_NAME = 'fastnetmon' -FASTNETMON_CONF = '/run/fastnetmon/fastnetmon.conf' -NETWORKS_CONF = '/run/fastnetmon/networks_list' -EXCLUDED_NETWORKS_CONF = '/run/fastnetmon/excluded_networks_list' -base_path = ['service', 'ids', 'ddos-protection'] - -class TestServiceIDS(VyOSUnitTestSHIM.TestCase): - @classmethod - def setUpClass(cls): - super(TestServiceIDS, cls).setUpClass() - - # ensure we can also run this test on a live system - so lets clean - # out the current configuration :) - cls.cli_delete(cls, base_path) - - def tearDown(self): - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - - # delete test config - self.cli_delete(base_path) - self.cli_commit() - - self.assertFalse(os.path.exists(FASTNETMON_CONF)) - self.assertFalse(process_named_running(PROCESS_NAME)) - - def test_fastnetmon(self): - networks = ['10.0.0.0/24', '10.5.5.0/24', '2001:db8:10::/64', '2001:db8:20::/64'] - excluded_networks = ['10.0.0.1/32', '2001:db8:10::1/128'] - interfaces = ['eth0', 'eth1'] - fps = '3500' - mbps = '300' - pps = '60000' - - self.cli_set(base_path + ['mode', 'mirror']) - # Required network! - with self.assertRaises(ConfigSessionError): - self.cli_commit() - for tmp in networks: - self.cli_set(base_path + ['network', tmp]) - - # optional excluded-network! - with self.assertRaises(ConfigSessionError): - self.cli_commit() - for tmp in excluded_networks: - self.cli_set(base_path + ['excluded-network', tmp]) - - # Required interface(s)! - with self.assertRaises(ConfigSessionError): - self.cli_commit() - for tmp in interfaces: - self.cli_set(base_path + ['listen-interface', tmp]) - - self.cli_set(base_path + ['direction', 'in']) - self.cli_set(base_path + ['threshold', 'general', 'fps', fps]) - self.cli_set(base_path + ['threshold', 'general', 'pps', pps]) - self.cli_set(base_path + ['threshold', 'general', 'mbps', mbps]) - - # commit changes - self.cli_commit() - - # Check configured port - config = read_file(FASTNETMON_CONF) - self.assertIn(f'mirror_afpacket = on', config) - self.assertIn(f'process_incoming_traffic = on', config) - self.assertIn(f'process_outgoing_traffic = off', config) - self.assertIn(f'ban_for_flows = on', config) - self.assertIn(f'threshold_flows = {fps}', config) - self.assertIn(f'ban_for_bandwidth = on', config) - self.assertIn(f'threshold_mbps = {mbps}', config) - self.assertIn(f'ban_for_pps = on', config) - self.assertIn(f'threshold_pps = {pps}', config) - # default - self.assertIn(f'enable_ban = on', config) - self.assertIn(f'enable_ban_ipv6 = on', config) - self.assertIn(f'ban_time = 1900', config) - - tmp = ','.join(interfaces) - self.assertIn(f'interfaces = {tmp}', config) - - - network_config = read_file(NETWORKS_CONF) - for tmp in networks: - self.assertIn(f'{tmp}', network_config) - - excluded_network_config = read_file(EXCLUDED_NETWORKS_CONF) - for tmp in excluded_networks: - self.assertIn(f'{tmp}', excluded_network_config) - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_ipoe-server.py b/smoketest/scripts/cli/test_service_ipoe-server.py index be03179bf..3b3c205cd 100755 --- a/smoketest/scripts/cli/test_service_ipoe-server.py +++ b/smoketest/scripts/cli/test_service_ipoe-server.py @@ -260,6 +260,63 @@ delegate={delegate_2_prefix},{delegate_mask},name={pool_name}""" tmp = ','.join(vlans) self.assertIn(f'{interface},{tmp}', conf['ipoe']['vlan-mon']) + def test_ipoe_server_static_client_ip_address(self): + mac_address = '08:00:27:2f:d8:06' + ip_address = '192.0.2.100' + + # Test configuration of local authentication for PPPoE server + self.basic_config() + # Rewrite authentication from basic_config + self.set( + [ + 'authentication', + 'interface', + interface, + 'mac', + mac_address, + 'ip-address', + ip_address, + ] + ) + self.set(['authentication', 'mode', 'local']) + # commit changes + self.cli_commit() + + # Validate configuration values + conf = ConfigParser(allow_no_value=True, delimiters='=', strict=False) + conf.read(self._config_file) + + # basic verification + self.verify(conf) + + # check local users + tmp = cmd(f'sudo cat {self._chap_secrets}') + regex = f'{interface}\s+\*\s+{mac_address}\s+{ip_address}' + tmp = re.findall(regex, tmp) + self.assertTrue(tmp) + + def test_ipoe_server_start_session(self): + start_session = 'auto' + + # Configuration of local authentication for PPPoE server + self.basic_config() + self.cli_commit() + + # Validate configuration values + conf = ConfigParser(allow_no_value=True, delimiters='=', strict=False) + conf.read(self._config_file) + # if 'start-session' option is not set the default value is 'dhcp' + self.assertIn(f'start=dhcpv4', conf['ipoe']['interface']) + + # change 'start-session' option to 'auto' + self.set(['interface', interface, 'start-session', start_session]) + self.cli_commit() + + # Validate changed configuration values + conf = ConfigParser(allow_no_value=True, delimiters='=', strict=False) + conf.read(self._config_file) + self.assertIn(f'start={start_session}', conf['ipoe']['interface']) + @unittest.skip("PPP is not a part of IPoE") def test_accel_ppp_options(self): pass diff --git a/smoketest/scripts/cli/test_service_lldp.py b/smoketest/scripts/cli/test_service_lldp.py index 9d72ef78f..c73707e0d 100755 --- a/smoketest/scripts/cli/test_service_lldp.py +++ b/smoketest/scripts/cli/test_service_lldp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022-2024 VyOS maintainers and contributors +# Copyright (C) 2022-2025 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 @@ -117,6 +117,8 @@ class TestServiceLLDP(VyOSUnitTestSHIM.TestCase): config = read_file(LLDPD_CONF) self.assertIn(f'configure ports {interface} med location elin "{elin}"', config) + # This is the CLI default mode + self.assertIn(f'configure ports {interface} lldp status rx-and-tx', config) self.assertIn(f'configure system interface pattern "{interface}"', config) def test_06_lldp_snmp(self): @@ -134,5 +136,50 @@ class TestServiceLLDP(VyOSUnitTestSHIM.TestCase): self.cli_delete(['service', 'snmp']) + def test_07_lldp_interface_mode(self): + interfaces = Section.interfaces('ethernet', vlan=False) + + # set interface mode to 'tx' + self.cli_set(base_path + ['interface', 'all']) + for interface in interfaces: + self.cli_set(base_path + ['interface', interface, 'mode', 'disable']) + # commit changes + self.cli_commit() + + # verify configuration + config = read_file(LLDPD_CONF) + for interface in interfaces: + self.assertIn(f'configure ports {interface} lldp status disable', config) + + # Change configuration to rx-only + for interface in interfaces: + self.cli_set(base_path + ['interface', interface, 'mode', 'rx']) + # commit changes + self.cli_commit() + # verify configuration + config = read_file(LLDPD_CONF) + for interface in interfaces: + self.assertIn(f'configure ports {interface} lldp status rx-only', config) + + # Change configuration to tx-only + for interface in interfaces: + self.cli_set(base_path + ['interface', interface, 'mode', 'tx']) + # commit changes + self.cli_commit() + # verify configuration + config = read_file(LLDPD_CONF) + for interface in interfaces: + self.assertIn(f'configure ports {interface} lldp status tx-only', config) + + # Change configuration to rx-only + for interface in interfaces: + self.cli_set(base_path + ['interface', interface, 'mode', 'rx-tx']) + # commit changes + self.cli_commit() + # verify configuration + config = read_file(LLDPD_CONF) + for interface in interfaces: + self.assertIn(f'configure ports {interface} lldp status rx-and-tx', config) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_mdns_repeater.py b/smoketest/scripts/cli/test_service_mdns_repeater.py index f2fb3b509..30e48683f 100755 --- a/smoketest/scripts/cli/test_service_mdns_repeater.py +++ b/smoketest/scripts/cli/test_service_mdns_repeater.py @@ -21,36 +21,45 @@ from base_vyostest_shim import VyOSUnitTestSHIM from configparser import ConfigParser from vyos.configsession import ConfigSessionError from vyos.utils.process import process_named_running +from vyos.xml_ref import default_value base_path = ['service', 'mdns', 'repeater'] intf_base = ['interfaces', 'dummy'] config_file = '/run/avahi-daemon/avahi-daemon.conf' - class TestServiceMDNSrepeater(VyOSUnitTestSHIM.TestCase): - def setUp(self): - # Start with a clean CLI instance - self.cli_delete(base_path) + @classmethod + def setUpClass(cls): + super(TestServiceMDNSrepeater, cls).setUpClass() - # Service required a configured IP address on the interface - self.cli_set(intf_base + ['dum10', 'address', '192.0.2.1/30']) - self.cli_set(intf_base + ['dum10', 'ipv6', 'address', 'no-default-link-local']) - self.cli_set(intf_base + ['dum20', 'address', '192.0.2.5/30']) - self.cli_set(intf_base + ['dum20', 'address', '2001:db8:0:2::5/64']) - self.cli_set(intf_base + ['dum30', 'address', '192.0.2.9/30']) - self.cli_set(intf_base + ['dum30', 'address', '2001:db8:0:2::9/64']) - self.cli_set(intf_base + ['dum40', 'address', '2001:db8:0:2::11/64']) - self.cli_commit() + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + + cls.cli_set(cls, intf_base + ['dum10', 'address', '192.0.2.1/30']) + cls.cli_set(cls, intf_base + ['dum10', 'ipv6', 'address', 'no-default-link-local']) + cls.cli_set(cls, intf_base + ['dum20', 'address', '192.0.2.5/30']) + cls.cli_set(cls, intf_base + ['dum20', 'address', '2001:db8:0:2::5/64']) + cls.cli_set(cls, intf_base + ['dum30', 'address', '192.0.2.9/30']) + cls.cli_set(cls, intf_base + ['dum30', 'address', '2001:db8:0:2::9/64']) + cls.cli_set(cls, intf_base + ['dum40', 'address', '2001:db8:0:2::11/64']) + + cls.cli_commit(cls) + + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, intf_base + ['dum10']) + cls.cli_delete(cls, intf_base + ['dum20']) + cls.cli_delete(cls, intf_base + ['dum30']) + cls.cli_delete(cls, intf_base + ['dum40']) + + cls.cli_commit(cls) def tearDown(self): # Check for running process self.assertTrue(process_named_running('avahi-daemon')) self.cli_delete(base_path) - self.cli_delete(intf_base + ['dum10']) - self.cli_delete(intf_base + ['dum20']) - self.cli_delete(intf_base + ['dum30']) - self.cli_delete(intf_base + ['dum40']) self.cli_commit() # Check that there is no longer a running process @@ -130,5 +139,38 @@ class TestServiceMDNSrepeater(VyOSUnitTestSHIM.TestCase): self.assertEqual(conf['server']['allow-interfaces'], 'dum30, dum40') self.assertEqual(conf['reflector']['enable-reflector'], 'yes') + def test_service_max_cache_entries(self): + cli_default_max_cache = default_value(base_path + ['cache-entries']) + self.cli_set(base_path) + + # Need at least two interfaces + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + ['interface', 'dum20']) + + # Need at least two interfaces + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + ['interface', 'dum30']) + + self.cli_commit() + + # Validate configuration values + conf = ConfigParser(delimiters='=') + conf.read(config_file) + self.assertEqual(conf['server']['cache-entries-max'], cli_default_max_cache) + + # Set max cache entries + cache_entries = '1234' + self.cli_set(base_path + ['cache-entries', cache_entries]) + + self.cli_commit() + + # Validate configuration values + conf = ConfigParser(delimiters='=') + conf.read(config_file) + + self.assertEqual(conf['server']['cache-entries-max'], cache_entries) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_monitoring_network_event.py b/smoketest/scripts/cli/test_service_monitoring_network_event.py new file mode 100644 index 000000000..3c9b4bf7f --- /dev/null +++ b/smoketest/scripts/cli/test_service_monitoring_network_event.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 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.utils.file import read_json + + +base_path = ['service', 'monitoring', 'network-event'] + + +def get_logger_config(): + return read_json('/run/vyos-network-event-logger.conf') + + +class TestMonitoringNetworkEvent(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(TestMonitoringNetworkEvent, cls).setUpClass() + + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + + def tearDown(self): + self.cli_delete(base_path) + self.cli_commit() + + def test_network_event_log(self): + expected_config = { + 'event': { + 'route': {}, + 'link': {}, + 'addr': {}, + 'neigh': {}, + 'rule': {}, + }, + 'queue_size': '10000' + } + + self.cli_set(base_path + ['event', 'route']) + self.cli_set(base_path + ['event', 'link']) + self.cli_set(base_path + ['event', 'addr']) + self.cli_set(base_path + ['event', 'neigh']) + self.cli_set(base_path + ['event', 'rule']) + self.cli_set(base_path + ['queue-size', '10000']) + self.cli_commit() + self.assertEqual(expected_config, get_logger_config()) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_monitoring_prometheus.py b/smoketest/scripts/cli/test_service_monitoring_prometheus.py new file mode 100755 index 000000000..6e7f8c808 --- /dev/null +++ b/smoketest/scripts/cli/test_service_monitoring_prometheus.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import unittest + +from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.utils.process import process_named_running +from vyos.utils.file import read_file + +NODE_EXPORTER_PROCESS_NAME = 'node_exporter' +FRR_EXPORTER_PROCESS_NAME = 'frr_exporter' +BLACKBOX_EXPORTER_PROCESS_NAME = 'blackbox_exporter' + +base_path = ['service', 'monitoring', 'prometheus'] +listen_if = 'dum3421' +listen_ip = '192.0.2.1' +node_exporter_service_file = '/etc/systemd/system/node_exporter.service' +frr_exporter_service_file = '/etc/systemd/system/frr_exporter.service' +blackbox_exporter_service_file = '/etc/systemd/system/blackbox_exporter.service' + + +class TestMonitoringPrometheus(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + # call base-classes classmethod + super(TestMonitoringPrometheus, cls).setUpClass() + # create a test interfaces + cls.cli_set( + cls, ['interfaces', 'dummy', listen_if, 'address', listen_ip + '/32'] + ) + + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, ['interfaces', 'dummy', listen_if]) + super(TestMonitoringPrometheus, cls).tearDownClass() + + def tearDown(self): + self.cli_delete(base_path) + self.cli_commit() + self.assertFalse(process_named_running(NODE_EXPORTER_PROCESS_NAME)) + self.assertFalse(process_named_running(FRR_EXPORTER_PROCESS_NAME)) + + def test_01_node_exporter(self): + self.cli_set(base_path + ['node-exporter', 'listen-address', listen_ip]) + self.cli_set(base_path + ['node-exporter', 'collectors', 'textfile']) + + # commit changes + self.cli_commit() + + file_content = read_file(node_exporter_service_file) + self.assertIn(f'{listen_ip}:9100', file_content) + + self.assertTrue(os.path.isdir('/run/node_exporter/collector')) + self.assertIn( + '--collector.textfile.directory=/run/node_exporter/collector', file_content + ) + + # Check for running process + self.assertTrue(process_named_running(NODE_EXPORTER_PROCESS_NAME)) + + def test_02_frr_exporter(self): + self.cli_set(base_path + ['frr-exporter', 'listen-address', listen_ip]) + + # commit changes + self.cli_commit() + + file_content = read_file(frr_exporter_service_file) + self.assertIn(f'{listen_ip}:9342', file_content) + + # Check for running process + self.assertTrue(process_named_running(FRR_EXPORTER_PROCESS_NAME)) + + def test_03_blackbox_exporter(self): + self.cli_set(base_path + ['blackbox-exporter', 'listen-address', listen_ip]) + + # commit changes + self.cli_commit() + + file_content = read_file(blackbox_exporter_service_file) + self.assertIn(f'{listen_ip}:9115', file_content) + + # Check for running process + self.assertTrue(process_named_running(BLACKBOX_EXPORTER_PROCESS_NAME)) + + def test_04_blackbox_exporter_with_config(self): + self.cli_set(base_path + ['blackbox-exporter', 'listen-address', listen_ip]) + self.cli_set( + base_path + + [ + 'blackbox-exporter', + 'modules', + 'dns', + 'name', + 'dns_ip4', + 'preferred-ip-protocol', + 'ipv4', + ] + ) + self.cli_set( + base_path + + [ + 'blackbox-exporter', + 'modules', + 'dns', + 'name', + 'dns_ip4', + 'query-name', + 'vyos.io', + ] + ) + self.cli_set( + base_path + + [ + 'blackbox-exporter', + 'modules', + 'dns', + 'name', + 'dns_ip4', + 'query-type', + 'A', + ] + ) + self.cli_set( + base_path + + [ + 'blackbox-exporter', + 'modules', + 'icmp', + 'name', + 'icmp_ip6', + 'preferred-ip-protocol', + 'ipv6', + ] + ) + + # commit changes + self.cli_commit() + + file_content = read_file(blackbox_exporter_service_file) + self.assertIn(f'{listen_ip}:9115', file_content) + + # Check for running process + self.assertTrue(process_named_running(BLACKBOX_EXPORTER_PROCESS_NAME)) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_monitoring_zabbix-agent.py b/smoketest/scripts/cli/test_service_monitoring_zabbix-agent.py index a60dae0a0..522f9df0f 100755 --- a/smoketest/scripts/cli/test_service_monitoring_zabbix-agent.py +++ b/smoketest/scripts/cli/test_service_monitoring_zabbix-agent.py @@ -23,6 +23,7 @@ from vyos.utils.file import read_file PROCESS_NAME = 'zabbix_agent2' ZABBIX_AGENT_CONF = '/run/zabbix/zabbix-agent2.conf' +ZABBIX_PSK_FILE = f'/run/zabbix/zabbix-agent2.psk' base_path = ['service', 'monitoring', 'zabbix-agent'] @@ -82,6 +83,26 @@ class TestZabbixAgent(VyOSUnitTestSHIM.TestCase): self.assertIn(f'Timeout={timeout}', config) self.assertIn(f'Hostname={hostname}', config) + def test_02_zabbix_agent_psk_auth(self): + secret = '8703ce4cb3f51279acba895e1421d69d8a7e2a18546d013d564ad87ac3957f29' + self.cli_set(base_path + ['server', '127.0.0.1']) + self.cli_set(base_path + ['authentication', 'mode', 'pre-shared-secret']) + self.cli_set(base_path + ['authentication', 'psk', 'id', 'smoke_test']) + self.cli_set(base_path + ['authentication', 'psk', 'secret', secret]) + self.cli_commit() + + config = read_file(ZABBIX_AGENT_CONF) + self.assertIn('TLSConnect=psk', config) + self.assertIn('TLSAccept=psk', config) + self.assertIn('TLSPSKIdentity=smoke_test', config) + self.assertIn(f'TLSPSKFile={ZABBIX_PSK_FILE}', config) + self.assertEqual(secret, read_file(ZABBIX_PSK_FILE)) + + secret = '8703ce4cb3f51279acba895e1421d69d8a7e2a18546d013d564ad87ac3957f88' + self.cli_set(base_path + ['authentication', 'psk', 'secret', secret]) + self.cli_commit() + self.assertEqual(secret, read_file(ZABBIX_PSK_FILE)) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_ntp.py b/smoketest/scripts/cli/test_service_ntp.py index ae45fe2f4..469d44eaa 100755 --- a/smoketest/scripts/cli/test_service_ntp.py +++ b/smoketest/scripts/cli/test_service_ntp.py @@ -21,6 +21,7 @@ from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.utils.process import cmd from vyos.utils.process import process_named_running +from vyos.xml_ref import default_value PROCESS_NAME = 'chronyd' NTP_CONF = '/run/chrony/chrony.conf' @@ -165,5 +166,99 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): self.assertIn(f'maxslewrate 1000', config) self.assertIn(f'smoothtime 400 0.001024 leaponly', config) + def test_interleave_option(self): + # "interleave" option differs from some others in that the + # name is not a 1:1 mapping from VyOS config + servers = ['192.0.2.1', '192.0.2.2'] + options = ['prefer'] + + for server in servers: + for option in options: + self.cli_set(base_path + ['server', server, option]) + self.cli_set(base_path + ['server', server, 'interleave']) + + # commit changes + self.cli_commit() + + # Check generated configuration + # this file must be read with higher permissions + config = cmd(f'sudo cat {NTP_CONF}') + self.assertIn('driftfile /run/chrony/drift', config) + self.assertIn('dumpdir /run/chrony', config) + self.assertIn('ntsdumpdir /run/chrony', config) + self.assertIn('clientloglimit 1048576', config) + self.assertIn('rtcsync', config) + self.assertIn('makestep 1.0 3', config) + self.assertIn('leapsectz right/UTC', config) + + for server in servers: + self.assertIn(f'server {server} iburst ' + ' '.join(options) + ' xleave', config) + + def test_offload_timestamp_default(self): + # Test offloading of NIC timestamp + servers = ['192.0.2.1', '192.0.2.2'] + ptp_port = '8319' + + for server in servers: + self.cli_set(base_path + ['server', server, 'ptp']) + + self.cli_set(base_path + ['ptp', 'port', ptp_port]) + self.cli_set(base_path + ['timestamp', 'interface', 'all']) + + # commit changes + self.cli_commit() + + # Check generated configuration + # this file must be read with higher permissions + config = cmd(f'sudo cat {NTP_CONF}') + self.assertIn('driftfile /run/chrony/drift', config) + self.assertIn('dumpdir /run/chrony', config) + self.assertIn('ntsdumpdir /run/chrony', config) + self.assertIn('clientloglimit 1048576', config) + self.assertIn('rtcsync', config) + self.assertIn('makestep 1.0 3', config) + self.assertIn('leapsectz right/UTC', config) + + for server in servers: + self.assertIn(f'server {server} iburst port {ptp_port}', config) + + self.assertIn('hwtimestamp *', config) + + def test_ptp_transport(self): + # Test offloading of NIC timestamp + servers = ['192.0.2.1', '192.0.2.2'] + options = ['prefer'] + + default_ptp_port = default_value(base_path + ['ptp', 'port']) + + for server in servers: + for option in options: + self.cli_set(base_path + ['server', server, option]) + self.cli_set(base_path + ['server', server, 'ptp']) + + # commit changes (expected to fail) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + # add the required top-level option and commit + self.cli_set(base_path + ['ptp']) + self.cli_commit() + + # Check generated configuration + # this file must be read with higher permissions + config = cmd(f'sudo cat {NTP_CONF}') + self.assertIn('driftfile /run/chrony/drift', config) + self.assertIn('dumpdir /run/chrony', config) + self.assertIn('ntsdumpdir /run/chrony', config) + self.assertIn('clientloglimit 1048576', config) + self.assertIn('rtcsync', config) + self.assertIn('makestep 1.0 3', config) + self.assertIn('leapsectz right/UTC', config) + + for server in servers: + self.assertIn(f'server {server} iburst ' + ' '.join(options) + f' port {default_ptp_port}', config) + + self.assertIn(f'ptpport {default_ptp_port}', config) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_router-advert.py b/smoketest/scripts/cli/test_service_router-advert.py index 6dbb6add4..33ce93ef4 100755 --- a/smoketest/scripts/cli/test_service_router-advert.py +++ b/smoketest/scripts/cli/test_service_router-advert.py @@ -252,6 +252,118 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase): tmp = get_config_value('AdvIntervalOpt') self.assertEqual(tmp, 'off') + def test_auto_ignore(self): + isp_prefix = '2001:db8::/64' + ula_prefixes = ['fd00::/64', 'fd01::/64'] + + # configure wildcard prefix + self.cli_set(base_path + ['prefix', '::/64']) + + # test auto-ignore CLI behaviors with no prefix overrides + # set auto-ignore for all three prefixes + self.cli_set(base_path + ['auto-ignore', isp_prefix]) + + for ula_prefix in ula_prefixes: + self.cli_set(base_path + ['auto-ignore', ula_prefix]) + + # commit and reload config + self.cli_commit() + config = read_file(RADVD_CONF) + + # ensure autoignoreprefixes block is generated in config file + tmp = f'autoignoreprefixes' + ' {' + self.assertIn(tmp, config) + + # ensure all three prefixes are contained in the block + self.assertIn(f' {isp_prefix};', config) + for ula_prefix in ula_prefixes: + self.assertIn(f' {ula_prefix};', config) + + # remove a prefix and verify it's gone + self.cli_delete(base_path + ['auto-ignore', ula_prefixes[1]]) + + # commit and reload config + self.cli_commit() + config = read_file(RADVD_CONF) + + self.assertNotIn(f' {ula_prefixes[1]};', config) + + # ensure remaining two prefixes are still present + self.assertIn(f' {ula_prefixes[0]};', config) + self.assertIn(f' {isp_prefix};', config) + + # remove the remaining two prefixes and verify the config block is gone + self.cli_delete(base_path + ['auto-ignore', ula_prefixes[0]]) + self.cli_delete(base_path + ['auto-ignore', isp_prefix]) + + # commit and reload config + self.cli_commit() + config = read_file(RADVD_CONF) + + tmp = f'autoignoreprefixes' + ' {' + self.assertNotIn(tmp, config) + + # test wildcard prefix overrides, with and without auto-ignore CLI configuration + newline = '\n' + left_curly = '{' + right_curly = '}' + + # override ULA prefixes + for ula_prefix in ula_prefixes: + self.cli_set(base_path + ['prefix', ula_prefix]) + + # commit and reload config + self.cli_commit() + config = read_file(RADVD_CONF) + + # ensure autoignoreprefixes block is generated in config file with both prefixes + tmp = f'autoignoreprefixes' + f' {left_curly}{newline} {ula_prefixes[0]};{newline} {ula_prefixes[1]};{newline} {right_curly};' + self.assertIn(tmp, config) + + # remove a ULA prefix and ensure there is only one prefix in the config block + self.cli_delete(base_path + ['prefix', ula_prefixes[0]]) + + # commit and reload config + self.cli_commit() + config = read_file(RADVD_CONF) + + # ensure autoignoreprefixes block is generated in config file with only one prefix + tmp = f'autoignoreprefixes' + f' {left_curly}{newline} {ula_prefixes[1]};{newline} {right_curly};' + self.assertIn(tmp, config) + + # exclude a prefix with auto-ignore CLI syntax + self.cli_set(base_path + ['auto-ignore', ula_prefixes[0]]) + + # commit and reload config + self.cli_commit() + config = read_file(RADVD_CONF) + + # verify that both prefixes appear in config block once again + tmp = f'autoignoreprefixes' + f' {left_curly}{newline} {ula_prefixes[0]};{newline} {ula_prefixes[1]};{newline} {right_curly};' + self.assertIn(tmp, config) + + # override first ULA prefix again + # first ULA is auto-ignored in CLI, it must appear only once in config + self.cli_set(base_path + ['prefix', ula_prefixes[0]]) + + # commit and reload config + self.cli_commit() + config = read_file(RADVD_CONF) + + # verify that both prefixes appear uniquely + tmp = f'autoignoreprefixes' + f' {left_curly}{newline} {ula_prefixes[0]};{newline} {ula_prefixes[1]};{newline} {right_curly};' + self.assertIn(tmp, config) + + # remove wildcard prefix and verify config block is gone + self.cli_delete(base_path + ['prefix', '::/64']) + + # commit and reload config + self.cli_commit() + config = read_file(RADVD_CONF) + + # verify config block is gone + tmp = f'autoignoreprefixes' + ' {' + self.assertNotIn(tmp, config) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_ssh.py b/smoketest/scripts/cli/test_service_ssh.py index d8e325eee..fa08a5b32 100755 --- a/smoketest/scripts/cli/test_service_ssh.py +++ b/smoketest/scripts/cli/test_service_ssh.py @@ -33,16 +33,32 @@ from vyos.xml_ref import default_value PROCESS_NAME = 'sshd' SSHD_CONF = '/run/sshd/sshd_config' base_path = ['service', 'ssh'] +pki_path = ['pki'] key_rsa = '/etc/ssh/ssh_host_rsa_key' key_dsa = '/etc/ssh/ssh_host_dsa_key' key_ed25519 = '/etc/ssh/ssh_host_ed25519_key' +trusted_user_ca_key = '/etc/ssh/trusted_user_ca_key' + def get_config_value(key): tmp = read_file(SSHD_CONF) tmp = re.findall(f'\n?{key}\s+(.*)', tmp) return tmp + +ca_root_cert_data = """ +MIIBcTCCARagAwIBAgIUDcAf1oIQV+6WRaW7NPcSnECQ/lUwCgYIKoZIzj0EAwIw +HjEcMBoGA1UEAwwTVnlPUyBzZXJ2ZXIgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjBa +Fw0zMjAyMTUxOTQxMjBaMB4xHDAaBgNVBAMME1Z5T1Mgc2VydmVyIHJvb3QgQ0Ew +WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ0y24GzKQf4aM2Ir12tI9yITOIzAUj +ZXyJeCmYI6uAnyAMqc4Q4NKyfq3nBi4XP87cs1jlC1P2BZ8MsjL5MdGWozIwMDAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRwC/YaieMEnjhYa7K3Flw/o0SFuzAK +BggqhkjOPQQDAgNJADBGAiEAh3qEj8vScsjAdBy5shXzXDVVOKWCPTdGrPKnu8UW +a2cCIQDlDgkzWmn5ujc5ATKz1fj+Se/aeqwh4QyoWCVTFLIxhQ== +""" + + class TestServiceSSH(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): @@ -98,27 +114,27 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase): # Check configured port port = get_config_value('Port')[0] - self.assertTrue("1234" in port) + self.assertTrue('1234' in port) # Check DNS usage dns = get_config_value('UseDNS')[0] - self.assertTrue("no" in dns) + self.assertTrue('no' in dns) # Check PasswordAuthentication pwd = get_config_value('PasswordAuthentication')[0] - self.assertTrue("no" in pwd) + self.assertTrue('no' in pwd) # Check loglevel loglevel = get_config_value('LogLevel')[0] - self.assertTrue("VERBOSE" in loglevel) + self.assertTrue('VERBOSE' in loglevel) # Check listen address address = get_config_value('ListenAddress')[0] - self.assertTrue("127.0.0.1" in address) + self.assertTrue('127.0.0.1' in address) # Check keepalive keepalive = get_config_value('ClientAliveInterval')[0] - self.assertTrue("100" in keepalive) + self.assertTrue('100' in keepalive) def test_ssh_multiple_listen_addresses(self): # Check if SSH service can be configured and runs with multiple @@ -197,7 +213,17 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase): test_command = 'uname -a' self.cli_set(base_path) - self.cli_set(['system', 'login', 'user', test_user, 'authentication', 'plaintext-password', test_pass]) + self.cli_set( + [ + 'system', + 'login', + 'user', + test_user, + 'authentication', + 'plaintext-password', + test_pass, + ] + ) # commit changes self.cli_commit() @@ -210,7 +236,9 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase): # Login with invalid credentials with self.assertRaises(paramiko.ssh_exception.AuthenticationException): - output, error = self.ssh_send_cmd(test_command, 'invalid_user', 'invalid_password') + output, error = self.ssh_send_cmd( + test_command, 'invalid_user', 'invalid_password' + ) self.cli_delete(['system', 'login', 'user', test_user]) self.cli_commit() @@ -250,7 +278,7 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase): sshguard_lines = [ f'THRESHOLD={threshold}', f'BLOCK_TIME={block_time}', - f'DETECTION_TIME={detect_time}' + f'DETECTION_TIME={detect_time}', ] tmp_sshguard_conf = read_file(SSHGUARD_CONFIG) @@ -268,12 +296,16 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase): self.assertFalse(process_named_running(SSHGUARD_PROCESS)) - # Network Device Collaborative Protection Profile def test_ssh_ndcpp(self): ciphers = ['aes128-cbc', 'aes128-ctr', 'aes256-cbc', 'aes256-ctr'] host_key_algs = ['sk-ssh-ed25519@openssh.com', 'ssh-rsa', 'ssh-ed25519'] - kexes = ['diffie-hellman-group14-sha1', 'ecdh-sha2-nistp256', 'ecdh-sha2-nistp384', 'ecdh-sha2-nistp521'] + kexes = [ + 'diffie-hellman-group14-sha1', + 'ecdh-sha2-nistp256', + 'ecdh-sha2-nistp384', + 'ecdh-sha2-nistp521', + ] macs = ['hmac-sha1', 'hmac-sha2-256', 'hmac-sha2-512'] rekey_time = '60' rekey_data = '1024' @@ -293,22 +325,29 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase): # commit changes self.cli_commit() - ssh_lines = ['Ciphers aes128-cbc,aes128-ctr,aes256-cbc,aes256-ctr', - 'HostKeyAlgorithms sk-ssh-ed25519@openssh.com,ssh-rsa,ssh-ed25519', - 'MACs hmac-sha1,hmac-sha2-256,hmac-sha2-512', - 'KexAlgorithms diffie-hellman-group14-sha1,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521', - 'RekeyLimit 1024M 60M' - ] + ssh_lines = [ + 'Ciphers aes128-cbc,aes128-ctr,aes256-cbc,aes256-ctr', + 'HostKeyAlgorithms sk-ssh-ed25519@openssh.com,ssh-rsa,ssh-ed25519', + 'MACs hmac-sha1,hmac-sha2-256,hmac-sha2-512', + 'KexAlgorithms diffie-hellman-group14-sha1,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521', + 'RekeyLimit 1024M 60M', + ] tmp_sshd_conf = read_file(SSHD_CONF) for line in ssh_lines: self.assertIn(line, tmp_sshd_conf) def test_ssh_pubkey_accepted_algorithm(self): - algs = ['ssh-ed25519', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', - 'ecdsa-sha2-nistp521', 'ssh-dss', 'ssh-rsa', 'rsa-sha2-256', - 'rsa-sha2-512' - ] + algs = [ + 'ssh-ed25519', + 'ecdsa-sha2-nistp256', + 'ecdsa-sha2-nistp384', + 'ecdsa-sha2-nistp521', + 'ssh-dss', + 'ssh-rsa', + 'rsa-sha2-256', + 'rsa-sha2-512', + ] expected = 'PubkeyAcceptedAlgorithms ' for alg in algs: @@ -320,6 +359,40 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase): tmp_sshd_conf = read_file(SSHD_CONF) self.assertIn(expected, tmp_sshd_conf) + def test_ssh_trusted_user_ca_key(self): + ca_cert_name = 'test_ca' + + # set pki ca <ca_cert_name> certificate <ca_key_data> + # set service ssh trusted-user-ca-key ca-certificate <ca_cert_name> + self.cli_set( + pki_path + + [ + 'ca', + ca_cert_name, + 'certificate', + ca_root_cert_data.replace('\n', ''), + ] + ) + self.cli_set( + base_path + ['trusted-user-ca-key', 'ca-certificate', ca_cert_name] + ) + self.cli_commit() + + trusted_user_ca_key_config = get_config_value('TrustedUserCAKeys') + self.assertIn(trusted_user_ca_key, trusted_user_ca_key_config) + + with open(trusted_user_ca_key, 'r') as file: + ca_key_contents = file.read() + self.assertIn(ca_root_cert_data, ca_key_contents) + + self.cli_delete(base_path + ['trusted-user-ca-key']) + self.cli_delete(['pki', 'ca', ca_cert_name]) + self.cli_commit() + + # Verify the CA key is removed + trusted_user_ca_key_config = get_config_value('TrustedUserCAKeys') + self.assertNotIn(trusted_user_ca_key, trusted_user_ca_key_config) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_webproxy.py b/smoketest/scripts/cli/test_service_webproxy.py index 2b3f6d21c..ab4707a61 100755 --- a/smoketest/scripts/cli/test_service_webproxy.py +++ b/smoketest/scripts/cli/test_service_webproxy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright (C) 2020-2025 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 @@ -297,6 +297,22 @@ class TestServiceWebProxy(VyOSUnitTestSHIM.TestCase): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) + def test_06_nocache_domain_proxy(self): + domains_nocache = ['test1.net', 'test2.net'] + self.cli_set(base_path + ['listen-address', listen_ip]) + for domain in domains_nocache: + self.cli_set(base_path + ['domain-noncache', domain]) + # commit changes + self.cli_commit() + + config = read_file(PROXY_CONF) + + for domain in domains_nocache: + self.assertIn(f'acl NOCACHE dstdomain {domain}', config) + self.assertIn(f'no_cache deny NOCACHE', config) + + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_system_flow-accounting.py b/smoketest/scripts/cli/test_system_flow-accounting.py index 515134220..9d7942789 100755 --- a/smoketest/scripts/cli/test_system_flow-accounting.py +++ b/smoketest/scripts/cli/test_system_flow-accounting.py @@ -97,111 +97,6 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase): self.assertIn(f'syslog: {syslog}', uacctd) self.assertIn(f'plugins: memory', uacctd) - def test_sflow(self): - sampling_rate = '4000' - source_address = '192.0.2.1' - dummy_if = 'dum3841' - agent_address = '192.0.2.2' - - sflow_server = { - '1.2.3.4' : { }, - '5.6.7.8' : { 'port' : '6000' }, - } - - self.cli_set(['interfaces', 'dummy', dummy_if, 'address', agent_address + '/32']) - self.cli_set(['interfaces', 'dummy', dummy_if, 'address', source_address + '/32']) - self.cli_set(base_path + ['disable-imt']) - - # You need to configure at least one interface for flow-accounting - with self.assertRaises(ConfigSessionError): - self.cli_commit() - for interface in Section.interfaces('ethernet'): - self.cli_set(base_path + ['interface', interface]) - - - # You need to configure at least one sFlow or NetFlow protocol, or not - # set "disable-imt" for flow-accounting - with self.assertRaises(ConfigSessionError): - self.cli_commit() - - self.cli_set(base_path + ['sflow', 'agent-address', agent_address]) - self.cli_set(base_path + ['sflow', 'sampling-rate', sampling_rate]) - self.cli_set(base_path + ['sflow', 'source-address', source_address]) - for server, server_config in sflow_server.items(): - self.cli_set(base_path + ['sflow', 'server', server]) - if 'port' in server_config: - self.cli_set(base_path + ['sflow', 'server', server, 'port', server_config['port']]) - - # commit changes - self.cli_commit() - - uacctd = read_file(uacctd_conf) - - # when 'disable-imt' is not configured on the CLI it must be present - self.assertNotIn(f'imt_path: /tmp/uacctd.pipe', uacctd) - self.assertNotIn(f'imt_mem_pools_number: 169', uacctd) - self.assertNotIn(f'plugins: memory', uacctd) - - for server, server_config in sflow_server.items(): - plugin_name = server.replace('.', '-') - if 'port' in server_config: - self.assertIn(f'sfprobe_receiver[sf_{plugin_name}]: {server}', uacctd) - else: - self.assertIn(f'sfprobe_receiver[sf_{plugin_name}]: {server}:6343', uacctd) - - self.assertIn(f'sfprobe_agentip[sf_{plugin_name}]: {agent_address}', uacctd) - self.assertIn(f'sampling_rate[sf_{plugin_name}]: {sampling_rate}', uacctd) - self.assertIn(f'sfprobe_source_ip[sf_{plugin_name}]: {source_address}', uacctd) - - self.cli_delete(['interfaces', 'dummy', dummy_if]) - - def test_sflow_ipv6(self): - sampling_rate = '100' - sflow_server = { - '2001:db8::1' : { }, - '2001:db8::2' : { 'port' : '6000' }, - } - - self.cli_set(base_path + ['disable-imt']) - - # You need to configure at least one interface for flow-accounting - with self.assertRaises(ConfigSessionError): - self.cli_commit() - for interface in Section.interfaces('ethernet'): - self.cli_set(base_path + ['interface', interface]) - - - # You need to configure at least one sFlow or NetFlow protocol, or not - # set "disable-imt" for flow-accounting - with self.assertRaises(ConfigSessionError): - self.cli_commit() - - self.cli_set(base_path + ['sflow', 'sampling-rate', sampling_rate]) - for server, server_config in sflow_server.items(): - self.cli_set(base_path + ['sflow', 'server', server]) - if 'port' in server_config: - self.cli_set(base_path + ['sflow', 'server', server, 'port', server_config['port']]) - - # commit changes - self.cli_commit() - - uacctd = read_file(uacctd_conf) - - # when 'disable-imt' is not configured on the CLI it must be present - self.assertNotIn(f'imt_path: /tmp/uacctd.pipe', uacctd) - self.assertNotIn(f'imt_mem_pools_number: 169', uacctd) - self.assertNotIn(f'plugins: memory', uacctd) - - for server, server_config in sflow_server.items(): - tmp_srv = server - tmp_srv = tmp_srv.replace(':', '-') - - if 'port' in server_config: - self.assertIn(f'sfprobe_receiver[sf_{tmp_srv}]: {bracketize_ipv6(server)}', uacctd) - else: - self.assertIn(f'sfprobe_receiver[sf_{tmp_srv}]: {bracketize_ipv6(server)}:6343', uacctd) - self.assertIn(f'sampling_rate[sf_{tmp_srv}]: {sampling_rate}', uacctd) - def test_netflow(self): engine_id = '33' max_flows = '667' @@ -288,8 +183,8 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase): self.assertIn(f'nfprobe_timeouts[nf_{tmp_srv}]: expint={tmo_expiry}:general={tmo_flow}:icmp={tmo_icmp}:maxlife={tmo_max}:tcp.fin={tmo_tcp_fin}:tcp={tmo_tcp_generic}:tcp.rst={tmo_tcp_rst}:udp={tmo_udp}', uacctd) - self.cli_delete(['interfaces', 'dummy', dummy_if]) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_system_ip.py b/smoketest/scripts/cli/test_system_ip.py index 5b0090237..5b6ef2046 100755 --- a/smoketest/scripts/cli/test_system_ip.py +++ b/smoketest/scripts/cli/test_system_ip.py @@ -18,11 +18,19 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError -from vyos.utils.file import read_file +from vyos.utils.system import sysctl_read +from vyos.xml_ref import default_value base_path = ['system', 'ip'] class TestSystemIP(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(TestSystemIP, cls).setUpClass() + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + def tearDown(self): self.cli_delete(base_path) self.cli_commit() @@ -30,47 +38,45 @@ class TestSystemIP(VyOSUnitTestSHIM.TestCase): def test_system_ip_forwarding(self): # Test if IPv4 forwarding can be disabled globally, default is '1' # which means forwarding enabled - all_forwarding = '/proc/sys/net/ipv4/conf/all/forwarding' - self.assertEqual(read_file(all_forwarding), '1') + self.assertEqual(sysctl_read('net.ipv4.conf.all.forwarding'), '1') self.cli_set(base_path + ['disable-forwarding']) self.cli_commit() + self.assertEqual(sysctl_read('net.ipv4.conf.all.forwarding'), '0') + frrconfig = self.getFRRconfig('', end='') + self.assertIn('no ip forwarding', frrconfig) - self.assertEqual(read_file(all_forwarding), '0') + self.cli_delete(base_path + ['disable-forwarding']) + self.cli_commit() + self.assertEqual(sysctl_read('net.ipv4.conf.all.forwarding'), '1') + frrconfig = self.getFRRconfig('', end='') + self.assertNotIn('no ip forwarding', frrconfig) def test_system_ip_multipath(self): # Test IPv4 multipathing options, options default to off -> '0' - use_neigh = '/proc/sys/net/ipv4/fib_multipath_use_neigh' - hash_policy = '/proc/sys/net/ipv4/fib_multipath_hash_policy' - - self.assertEqual(read_file(use_neigh), '0') - self.assertEqual(read_file(hash_policy), '0') + self.assertEqual(sysctl_read('net.ipv4.fib_multipath_use_neigh'), '0') + self.assertEqual(sysctl_read('net.ipv4.fib_multipath_hash_policy'), '0') self.cli_set(base_path + ['multipath', 'ignore-unreachable-nexthops']) self.cli_set(base_path + ['multipath', 'layer4-hashing']) self.cli_commit() - self.assertEqual(read_file(use_neigh), '1') - self.assertEqual(read_file(hash_policy), '1') + self.assertEqual(sysctl_read('net.ipv4.fib_multipath_use_neigh'), '1') + self.assertEqual(sysctl_read('net.ipv4.fib_multipath_hash_policy'), '1') def test_system_ip_arp_table_size(self): - # Maximum number of entries to keep in the ARP cache, the - # default is 8k + cli_default = int(default_value(base_path + ['arp', 'table-size'])) + def _verify_gc_thres(table_size): + self.assertEqual(sysctl_read('net.ipv4.neigh.default.gc_thresh3'), str(table_size)) + self.assertEqual(sysctl_read('net.ipv4.neigh.default.gc_thresh2'), str(table_size // 2)) + self.assertEqual(sysctl_read('net.ipv4.neigh.default.gc_thresh1'), str(table_size // 8)) - gc_thresh3 = '/proc/sys/net/ipv4/neigh/default/gc_thresh3' - gc_thresh2 = '/proc/sys/net/ipv4/neigh/default/gc_thresh2' - gc_thresh1 = '/proc/sys/net/ipv4/neigh/default/gc_thresh1' - self.assertEqual(read_file(gc_thresh3), '8192') - self.assertEqual(read_file(gc_thresh2), '4096') - self.assertEqual(read_file(gc_thresh1), '1024') + _verify_gc_thres(cli_default) for size in [1024, 2048, 4096, 8192, 16384, 32768]: self.cli_set(base_path + ['arp', 'table-size', str(size)]) self.cli_commit() - - self.assertEqual(read_file(gc_thresh3), str(size)) - self.assertEqual(read_file(gc_thresh2), str(size // 2)) - self.assertEqual(read_file(gc_thresh1), str(size // 8)) + _verify_gc_thres(size) def test_system_ip_protocol_route_map(self): protocols = ['any', 'babel', 'bgp', 'connected', 'eigrp', 'isis', @@ -83,7 +89,7 @@ class TestSystemIP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify route-map properly applied to FRR - frrconfig = self.getFRRconfig('ip protocol', end='', daemon='zebra') + frrconfig = self.getFRRconfig('ip protocol', end='') for protocol in protocols: self.assertIn(f'ip protocol {protocol} route-map route-map-{protocol}', frrconfig) @@ -94,7 +100,7 @@ class TestSystemIP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify route-map properly applied to FRR - frrconfig = self.getFRRconfig('ip protocol', end='', daemon='zebra') + frrconfig = self.getFRRconfig('ip protocol', end='') self.assertNotIn(f'ip protocol', frrconfig) def test_system_ip_protocol_non_existing_route_map(self): @@ -113,13 +119,13 @@ class TestSystemIP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['nht', 'no-resolve-via-default']) self.cli_commit() # Verify CLI config applied to FRR - frrconfig = self.getFRRconfig('', end='', daemon='zebra') + frrconfig = self.getFRRconfig('', end='') self.assertIn(f'no ip nht resolve-via-default', frrconfig) self.cli_delete(base_path + ['nht', 'no-resolve-via-default']) self.cli_commit() # Verify CLI config removed to FRR - frrconfig = self.getFRRconfig('', end='', daemon='zebra') + frrconfig = self.getFRRconfig('', end='') self.assertNotIn(f'no ip nht resolve-via-default', frrconfig) if __name__ == '__main__': diff --git a/smoketest/scripts/cli/test_system_ipv6.py b/smoketest/scripts/cli/test_system_ipv6.py index 0c77c1dd4..26f281bb4 100755 --- a/smoketest/scripts/cli/test_system_ipv6.py +++ b/smoketest/scripts/cli/test_system_ipv6.py @@ -19,16 +19,19 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError -from vyos.utils.file import read_file +from vyos.utils.system import sysctl_read +from vyos.xml_ref import default_value base_path = ['system', 'ipv6'] -file_forwarding = '/proc/sys/net/ipv6/conf/all/forwarding' -file_disable = '/proc/sys/net/ipv6/conf/all/disable_ipv6' -file_dad = '/proc/sys/net/ipv6/conf/all/accept_dad' -file_multipath = '/proc/sys/net/ipv6/fib_multipath_hash_policy' - class TestSystemIPv6(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(TestSystemIPv6, cls).setUpClass() + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + def tearDown(self): self.cli_delete(base_path) self.cli_commit() @@ -36,16 +39,23 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase): def test_system_ipv6_forwarding(self): # Test if IPv6 forwarding can be disabled globally, default is '1' # which means forwearding enabled - self.assertEqual(read_file(file_forwarding), '1') + self.assertEqual(sysctl_read('net.ipv6.conf.all.forwarding'), '1') self.cli_set(base_path + ['disable-forwarding']) self.cli_commit() + self.assertEqual(sysctl_read('net.ipv6.conf.all.forwarding'), '0') + frrconfig = self.getFRRconfig('', end='') + self.assertIn('no ipv6 forwarding', frrconfig) - self.assertEqual(read_file(file_forwarding), '0') + self.cli_delete(base_path + ['disable-forwarding']) + self.cli_commit() + self.assertEqual(sysctl_read('net.ipv6.conf.all.forwarding'), '1') + frrconfig = self.getFRRconfig('', end='') + self.assertNotIn('no ipv6 forwarding', frrconfig) def test_system_ipv6_strict_dad(self): # This defaults to 1 - self.assertEqual(read_file(file_dad), '1') + self.assertEqual(sysctl_read('net.ipv6.conf.all.accept_dad'), '1') # Do not assign any IPv6 address on interfaces, this requires a reboot # which can not be tested, but we can read the config file :) @@ -53,11 +63,11 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify configuration file - self.assertEqual(read_file(file_dad), '2') + self.assertEqual(sysctl_read('net.ipv6.conf.all.accept_dad'), '2') def test_system_ipv6_multipath(self): # This defaults to 0 - self.assertEqual(read_file(file_multipath), '0') + self.assertEqual(sysctl_read('net.ipv6.fib_multipath_hash_policy'), '0') # Do not assign any IPv6 address on interfaces, this requires a reboot # which can not be tested, but we can read the config file :) @@ -65,26 +75,24 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify configuration file - self.assertEqual(read_file(file_multipath), '1') + self.assertEqual(sysctl_read('net.ipv6.fib_multipath_hash_policy'), '1') def test_system_ipv6_neighbor_table_size(self): # Maximum number of entries to keep in the ARP cache, the # default is 8192 + cli_default = int(default_value(base_path + ['neighbor', 'table-size'])) - gc_thresh3 = '/proc/sys/net/ipv6/neigh/default/gc_thresh3' - gc_thresh2 = '/proc/sys/net/ipv6/neigh/default/gc_thresh2' - gc_thresh1 = '/proc/sys/net/ipv6/neigh/default/gc_thresh1' - self.assertEqual(read_file(gc_thresh3), '8192') - self.assertEqual(read_file(gc_thresh2), '4096') - self.assertEqual(read_file(gc_thresh1), '1024') + def _verify_gc_thres(table_size): + self.assertEqual(sysctl_read('net.ipv6.neigh.default.gc_thresh3'), str(table_size)) + self.assertEqual(sysctl_read('net.ipv6.neigh.default.gc_thresh2'), str(table_size // 2)) + self.assertEqual(sysctl_read('net.ipv6.neigh.default.gc_thresh1'), str(table_size // 8)) + + _verify_gc_thres(cli_default) for size in [1024, 2048, 4096, 8192, 16384, 32768]: self.cli_set(base_path + ['neighbor', 'table-size', str(size)]) self.cli_commit() - - self.assertEqual(read_file(gc_thresh3), str(size)) - self.assertEqual(read_file(gc_thresh2), str(size // 2)) - self.assertEqual(read_file(gc_thresh1), str(size // 8)) + _verify_gc_thres(size) def test_system_ipv6_protocol_route_map(self): protocols = ['any', 'babel', 'bgp', 'connected', 'isis', @@ -99,7 +107,7 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify route-map properly applied to FRR - frrconfig = self.getFRRconfig('ipv6 protocol', end='', daemon='zebra') + frrconfig = self.getFRRconfig('ipv6 protocol', end='') for protocol in protocols: # VyOS and FRR use a different name for OSPFv3 (IPv6) if protocol == 'ospfv3': @@ -113,7 +121,7 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify route-map properly applied to FRR - frrconfig = self.getFRRconfig('ipv6 protocol', end='', daemon='zebra') + frrconfig = self.getFRRconfig('ipv6 protocol', end='') self.assertNotIn(f'ipv6 protocol', frrconfig) def test_system_ipv6_protocol_non_existing_route_map(self): @@ -132,13 +140,13 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['nht', 'no-resolve-via-default']) self.cli_commit() # Verify CLI config applied to FRR - frrconfig = self.getFRRconfig('', end='', daemon='zebra') + frrconfig = self.getFRRconfig('', end='') self.assertIn(f'no ipv6 nht resolve-via-default', frrconfig) self.cli_delete(base_path + ['nht', 'no-resolve-via-default']) self.cli_commit() # Verify CLI config removed to FRR - frrconfig = self.getFRRconfig('', end='', daemon='zebra') + frrconfig = self.getFRRconfig('', end='') self.assertNotIn(f'no ipv6 nht resolve-via-default', frrconfig) if __name__ == '__main__': diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py index 28abba012..71dec68d8 100755 --- a/smoketest/scripts/cli/test_system_login.py +++ b/smoketest/scripts/cli/test_system_login.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright (C) 2019-2025 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 @@ -14,23 +14,37 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import os import re import unittest +import jinja2 +import secrets +import string +import paramiko +import shutil from base_vyostest_shim import VyOSUnitTestSHIM from gzip import GzipFile -from subprocess import Popen, PIPE +from subprocess import Popen +from subprocess import PIPE from pwd import getpwall from vyos.configsession import ConfigSessionError +from vyos.configquery import ConfigTreeQuery from vyos.utils.auth import get_current_user from vyos.utils.process import cmd from vyos.utils.file import read_file +from vyos.utils.file import write_file from vyos.template import inc_ip +from vyos.template import is_ipv6 +from vyos.xml_ref import default_value base_path = ['system', 'login'] users = ['vyos1', 'vyos-roxx123', 'VyOS-123_super.Nice'] +weak_passwd_user = ['test_user', 'passWord1'] + +ssh_test_command = '/opt/vyatta/bin/vyatta-op-cmd-wrapper show version' ssh_pubkey = """ AAAAB3NzaC1yc2EAAAADAQABAAABgQD0NuhUOEtMIKnUVFIHoFatqX/c4mjerXyF @@ -44,6 +58,71 @@ pHJz8umqkxy3hfw0K7BRFtjWd63sbOP8Q/SDV7LPaIfIxenA9zv2rY7y+AIqTmSr TTSb0X1zPGxPIRFy5GoGtO9Mm5h4OZk= """ +tac_image = 'docker.io/lfkeitel/tacacs_plus:alpine' +tac_image_path = '/usr/share/vyos/tacplus-alpine.tar' +TAC_PLUS_TMPL_SRC = """ +id = spawnd { + debug redirect = /dev/stdout + listen = { port = 49 } + spawn = { + instances min = 1 + instances max = 10 + } + background = no +} + +id = tac_plus { + debug = ALL + log = stdout { + destination = /dev/stdout + } + authorization log group = yes + authentication log = stdout + authorization log = stdout + accounting log = stdout + + host = smoketest { + address = {{ source_address }}/32 + enable = clear enable + key = {{ tacacs_secret }} + } + + group = admin { + default service = permit + enable = permit + service = shell { + default command = permit + default attribute = permit + set priv-lvl = 15 + } + } + + user = {{ username }} { + password = clear {{ password }} + member = admin + } +} + +""" + +radius_image = 'docker.io/dchidell/radius-web:latest' +radius_image_path = '/usr/share/vyos/radius-latest.tar' +RADIUS_CLIENTS_TMPL_SRC = """ +client SMOKETEST { + secret = {{ radius_key }} + nastype = other + ipaddr = {{ source_address }} +} + +""" +RADIUS_USERS_TMPL_SRC = """ +# User configuration +{{ username }} Cleartext-Password := "{{ password }}" + Service-Type = NAS-Prompt-User, + Cisco-AVPair = "shell:priv-lvl=15" + +""" + class TestSystemLogin(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): @@ -54,6 +133,37 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): cls.cli_delete(cls, base_path + ['radius']) cls.cli_delete(cls, base_path + ['tacacs']) + # Load images for smoketest provided in vyos-1x-smoketest + if not os.path.exists(tac_image_path): + cls.fail(cls, f'{tac_image} image not available') + cmd(f'sudo podman load -i {tac_image_path}') + + if not os.path.exists(radius_image_path): + cls.fail(cls, f'{radius_image} image not available') + cmd(f'sudo podman load -i {radius_image_path}') + + cls.ssh_test_command_result = cls.op_mode(cls, ['show', 'version']) + + # Dynamically start SSH service if it's not running + config = ConfigTreeQuery() + cls.is_sshd_pre_test = config.exists(['service', 'sshd']) + if not cls.is_sshd_pre_test: + # Start SSH service + cls.cli_set(cls, ['service', 'ssh']) + + @classmethod + def tearDownClass(cls): + # Stop SSH service - if it was not running before starting the test + if not cls.is_sshd_pre_test: + cls.cli_set(cls, ['service', 'ssh']) + cls.cli_commit(cls) + + super(TestSystemLogin, cls).tearDownClass() + + # Cleanup container images + cmd(f'sudo podman image rm -f {tac_image}') + cmd(f'sudo podman image rm -f {radius_image}') + def tearDown(self): # Delete individual users from configuration for user in users: @@ -83,29 +193,28 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path + ['user', system_user]) def test_system_login_user(self): - # Check if user can be created and we can SSH to localhost - self.cli_set(['service', 'ssh', 'port', '22']) - for user in users: - name = "VyOS Roxx " + user - home_dir = "/tmp/" + user + name = f'VyOS Roxx {user}' + passwd = f'{user}-pSWd-t3st' + home_dir = f'/tmp/smoketest/{user}' - self.cli_set(base_path + ['user', user, 'authentication', 'plaintext-password', user]) - self.cli_set(base_path + ['user', user, 'full-name', 'VyOS Roxx']) + self.cli_set(base_path + ['user', user, 'authentication', 'plaintext-password', passwd]) + self.cli_set(base_path + ['user', user, 'full-name', name]) self.cli_set(base_path + ['user', user, 'home-directory', home_dir]) self.cli_commit() for user in users: + passwd = f'{user}-pSWd-t3st' tmp = ['su','-', user] proc = Popen(tmp, stdin=PIPE, stdout=PIPE, stderr=PIPE) - tmp = "{}\nuname -a".format(user) + tmp = f'{passwd}\nuname -a' proc.stdin.write(tmp.encode()) proc.stdin.flush() (stdout, stderr) = proc.communicate() # stdout is something like this: - # b'Linux LR1.wue3 5.10.61-amd64-vyos #1 SMP Fri Aug 27 08:55:46 UTC 2021 x86_64 GNU/Linux\n' + # b'Linux vyos 6.6.66-vyos 6.6.66-vyos #1 SMP Mon Dec 30 19:05:15 UTC 2024 x86_64 GNU/Linux\n' self.assertTrue(len(stdout) > 40) locked_user = users[0] @@ -123,6 +232,16 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): tmp = cmd(f'sudo passwd -S {locked_user}') self.assertIn(f'{locked_user} P ', tmp) + def test_system_login_weak_password_warning(self): + self.cli_set(base_path + [ + 'user', weak_passwd_user[0], 'authentication', + 'plaintext-password', weak_passwd_user[1] + ]) + + out = self.cli_commit().strip() + + self.assertIn('WARNING: The password complexity is too low', out) + self.cli_delete(base_path + ['user', weak_passwd_user[0]]) def test_system_login_otp(self): otp_user = 'otp-test_user' @@ -172,17 +291,71 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): self.assertIn(f'{option}=y', kernel_config) def test_system_login_radius_ipv4(self): - # Verify generated RADIUS configuration files + radius_servers = ['100.64.0.4', '100.64.0.5'] + radius_source = '100.64.0.1' + self._system_login_radius_test_helper(radius_servers, radius_source) - radius_key = 'VyOSsecretVyOS' - radius_server = '172.16.100.10' - radius_source = '127.0.0.1' - radius_port = '2000' - radius_timeout = '1' + def test_system_login_radius_ipv6(self): + radius_servers = ['2001:db8::4', '2001:db8::5'] + radius_source = '2001:db8::1' + self._system_login_radius_test_helper(radius_servers, radius_source) + + def _system_login_radius_test_helper(self, radius_servers: list, radius_source: str): + # Verify generated RADIUS configuration files + radius_key = ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(10)) + + default_port = default_value(base_path + ['radius', 'server', radius_servers[0], 'port']) + default_timeout = default_value(base_path + ['radius', 'server', radius_servers[0], 'timeout']) + + dummy_if = 'dum12760' + + # Load container image for FreeRADIUS server + radius_config = '/tmp/smoketest-radius-server' + radius_container_path = ['container', 'name', 'radius-1'] + + # Generate random string with 10 digits + username = 'radius-admin' + password = ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(10)) + radius_source_mask = '32' + if is_ipv6(radius_source): + radius_source_mask = '128' + radius_test_user = { + 'username' : username, + 'password' : password, + 'radius_key' : radius_key, + 'source_address' : f'{radius_source}/{radius_source_mask}' + } + + tmpl = jinja2.Template(RADIUS_CLIENTS_TMPL_SRC) + write_file(f'{radius_config}/clients.cfg', tmpl.render(radius_test_user)) + + tmpl = jinja2.Template(RADIUS_USERS_TMPL_SRC) + write_file(f'{radius_config}/users', tmpl.render(radius_test_user)) + + # Start tac_plus container + self.cli_set(radius_container_path + ['allow-host-networks']) + self.cli_set(radius_container_path + ['image', radius_image]) + self.cli_set(radius_container_path + ['volume', 'clients', 'destination', '/etc/raddb/clients.conf']) + self.cli_set(radius_container_path + ['volume', 'clients', 'mode', 'ro']) + self.cli_set(radius_container_path + ['volume', 'clients', 'source', f'{radius_config}/clients.cfg']) + self.cli_set(radius_container_path + ['volume', 'users', 'destination', '/etc/raddb/users']) + self.cli_set(radius_container_path + ['volume', 'users', 'mode', 'ro']) + self.cli_set(radius_container_path + ['volume', 'users', 'source', f'{radius_config}/users']) + + # Start container + self.cli_commit() - self.cli_set(base_path + ['radius', 'server', radius_server, 'key', radius_key]) - self.cli_set(base_path + ['radius', 'server', radius_server, 'port', radius_port]) - self.cli_set(base_path + ['radius', 'server', radius_server, 'timeout', radius_timeout]) + # Deinfine RADIUS servers + for radius_server in radius_servers: + # Use this system as "remote" RADIUS server + dummy_address_mask = '32' + if is_ipv6(radius_server): + dummy_address_mask = '128' + self.cli_set(['interfaces', 'dummy', dummy_if, 'address', f'{radius_server}/{dummy_address_mask}']) + self.cli_set(base_path + ['radius', 'server', radius_server, 'key', radius_key]) + + # Define RADIUS traffic source address + self.cli_set(['interfaces', 'dummy', dummy_if, 'address', f'{radius_source}/{radius_source_mask}']) self.cli_set(base_path + ['radius', 'source-address', radius_source]) self.cli_set(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)]) @@ -195,10 +368,13 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): # this file must be read with higher permissions pam_radius_auth_conf = cmd('sudo cat /etc/pam_radius_auth.conf') - tmp = re.findall(r'\n?{}:{}\s+{}\s+{}\s+{}'.format(radius_server, - radius_port, radius_key, radius_timeout, - radius_source), pam_radius_auth_conf) - self.assertTrue(tmp) + + for radius_server in radius_servers: + if is_ipv6(radius_server): + # it is essential to escape the [] brackets when searching with a regex + radius_server = rf'\[{radius_server}\]' + tmp = re.findall(rf'\n?{radius_server}:{default_port}\s+{radius_key}\s+{default_timeout}\s+{radius_source}', pam_radius_auth_conf) + self.assertTrue(tmp) # required, static options self.assertIn('priv-lvl 15', pam_radius_auth_conf) @@ -225,59 +401,26 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): tmp = re.findall(r'group:\s+mapname\s+files', nsswitch_conf) self.assertTrue(tmp) - def test_system_login_radius_ipv6(self): - # Verify generated RADIUS configuration files + # Login with proper credentials + out, err = self.ssh_send_cmd(ssh_test_command, username, password) + # verify login + self.assertFalse(err) + self.assertEqual(out, self.ssh_test_command_result) - radius_key = 'VyOS-VyOS' - radius_server = '2001:db8::1' - radius_source = '::1' - radius_port = '4000' - radius_timeout = '4' - - self.cli_set(base_path + ['radius', 'server', radius_server, 'key', radius_key]) - self.cli_set(base_path + ['radius', 'server', radius_server, 'port', radius_port]) - self.cli_set(base_path + ['radius', 'server', radius_server, 'timeout', radius_timeout]) - self.cli_set(base_path + ['radius', 'source-address', radius_source]) - self.cli_set(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)]) - - # check validate() - Only one IPv4 source-address supported - with self.assertRaises(ConfigSessionError): - self.cli_commit() - self.cli_delete(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)]) + # Login with invalid credentials + with self.assertRaises(paramiko.ssh_exception.AuthenticationException): + _, _ = self.ssh_send_cmd(ssh_test_command, username, f'{password}1') + # Remove RADIUS configuration + self.cli_delete(base_path + ['radius']) + # Remove RADIUS container + self.cli_delete(radius_container_path) + # Remove dummy interface + self.cli_delete(['interfaces', 'dummy', dummy_if]) self.cli_commit() - # this file must be read with higher permissions - pam_radius_auth_conf = cmd('sudo cat /etc/pam_radius_auth.conf') - tmp = re.findall(r'\n?\[{}\]:{}\s+{}\s+{}\s+\[{}\]'.format(radius_server, - radius_port, radius_key, radius_timeout, - radius_source), pam_radius_auth_conf) - self.assertTrue(tmp) - - # required, static options - self.assertIn('priv-lvl 15', pam_radius_auth_conf) - self.assertIn('mapped_priv_user radius_priv_user', pam_radius_auth_conf) - - # PAM - pam_common_account = read_file('/etc/pam.d/common-account') - self.assertIn('pam_radius_auth.so', pam_common_account) - - pam_common_auth = read_file('/etc/pam.d/common-auth') - self.assertIn('pam_radius_auth.so', pam_common_auth) - - pam_common_session = read_file('/etc/pam.d/common-session') - self.assertIn('pam_radius_auth.so', pam_common_session) - - pam_common_session_noninteractive = read_file('/etc/pam.d/common-session-noninteractive') - self.assertIn('pam_radius_auth.so', pam_common_session_noninteractive) - - # NSS - nsswitch_conf = read_file('/etc/nsswitch.conf') - tmp = re.findall(r'passwd:\s+mapuid\s+files\s+mapname', nsswitch_conf) - self.assertTrue(tmp) - - tmp = re.findall(r'group:\s+mapname\s+files', nsswitch_conf) - self.assertTrue(tmp) + # Remove rendered tac_plus daemon configuration + shutil.rmtree(radius_config) def test_system_login_max_login_session(self): max_logins = '2' @@ -300,11 +443,46 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path + ['max-login-session']) def test_system_login_tacacs(self): - tacacs_secret = 'tac_plus_key' + tacacs_secret = ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(10)) tacacs_servers = ['100.64.0.11', '100.64.0.12'] + source_address = '100.64.0.1' + dummy_if = 'dum12759' + + # Load container image for lac_plus daemon + tac_plus_config = '/tmp/smoketest-tacacs-server' + tac_container_path = ['container', 'name', 'tacacs-1'] + + # Generate random string with 10 digits + username = 'tactest' + password = ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(10)) + tac_test_user = { + 'username' : username, + 'password' : password, + 'tacacs_secret' : tacacs_secret, + 'source_address' : source_address, + } + + tmpl = jinja2.Template(TAC_PLUS_TMPL_SRC) + write_file(f'{tac_plus_config}/tac_plus.cfg', tmpl.render(tac_test_user)) + + # Start tac_plus container + self.cli_set(tac_container_path + ['allow-host-networks']) + self.cli_set(tac_container_path + ['image', tac_image]) + self.cli_set(tac_container_path + ['volume', 'config', 'destination', '/etc/tac_plus']) + self.cli_set(tac_container_path + ['volume', 'config', 'mode', 'ro']) + self.cli_set(tac_container_path + ['volume', 'config', 'source', tac_plus_config]) + + # Start container + self.cli_commit() + + # Define TACACS traffic source address + self.cli_set(['interfaces', 'dummy', dummy_if, 'address', f'{source_address}/32']) + self.cli_set(base_path + ['tacacs', 'source-address', source_address]) - # Enable TACACS + # Define TACACS servers for server in tacacs_servers: + # Use this system as "remote" TACACS server + self.cli_set(['interfaces', 'dummy', dummy_if, 'address', f'{server}/32']) self.cli_set(base_path + ['tacacs', 'server', server, 'key', tacacs_secret]) self.cli_commit() @@ -328,6 +506,11 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): self.assertIn('service=shell', pam_tacacs_conf) self.assertIn('protocol=ssh', pam_tacacs_conf) + # Verify configured TACACS source address + self.assertIn(f'source_ip={source_address}', pam_tacacs_conf) + self.assertIn(f'source_ip={source_address}', nss_tacacs_conf) + + # Verify configured TACACS servers for server in tacacs_servers: self.assertIn(f'secret={tacacs_secret}', pam_tacacs_conf) self.assertIn(f'server={server}', pam_tacacs_conf) @@ -335,6 +518,27 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): self.assertIn(f'secret={tacacs_secret}', nss_tacacs_conf) self.assertIn(f'server={server}', nss_tacacs_conf) + # Login with proper credentials + out, err = self.ssh_send_cmd(ssh_test_command, username, password) + # verify login + self.assertFalse(err) + self.assertEqual(out, self.ssh_test_command_result) + + # Login with invalid credentials + with self.assertRaises(paramiko.ssh_exception.AuthenticationException): + _, _ = self.ssh_send_cmd(ssh_test_command, username, f'{password}1') + + # Remove TACACS configuration + self.cli_delete(base_path + ['tacacs']) + # Remove tac_plus container + self.cli_delete(tac_container_path) + # Remove dummy interface + self.cli_delete(['interfaces', 'dummy', dummy_if]) + self.cli_commit() + + # Remove rendered tac_plus daemon configuration + shutil.rmtree(tac_plus_config) + def test_delete_current_user(self): current_user = get_current_user() diff --git a/smoketest/scripts/cli/test_system_option.py b/smoketest/scripts/cli/test_system_option.py index ffb1d76ae..c7f8c1f3e 100755 --- a/smoketest/scripts/cli/test_system_option.py +++ b/smoketest/scripts/cli/test_system_option.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024 VyOS maintainers and contributors +# Copyright (C) 2024-2025 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 @@ -16,10 +16,15 @@ import os import unittest + from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSessionError +from vyos.utils.cpu import get_cpus from vyos.utils.file import read_file from vyos.utils.process import is_systemd_service_active from vyos.utils.system import sysctl_read +from vyos.system import image base_path = ['system', 'option'] @@ -59,6 +64,7 @@ class TestSystemOption(VyOSUnitTestSHIM.TestCase): def test_performance(self): tuned_service = 'tuned.service' + path = ['system', 'sysctl', 'parameter'] self.assertFalse(is_systemd_service_active(tuned_service)) @@ -67,11 +73,11 @@ class TestSystemOption(VyOSUnitTestSHIM.TestCase): gc_thresh2 = '262000' gc_thresh3 = '524000' - self.cli_set(['system', 'sysctl', 'parameter', 'net.ipv4.neigh.default.gc_thresh1', 'value', gc_thresh1]) - self.cli_set(['system', 'sysctl', 'parameter', 'net.ipv4.neigh.default.gc_thresh2', 'value', gc_thresh2]) - self.cli_set(['system', 'sysctl', 'parameter', 'net.ipv4.neigh.default.gc_thresh3', 'value', gc_thresh3]) + self.cli_set(path + ['net.ipv4.neigh.default.gc_thresh1', 'value', gc_thresh1]) + self.cli_set(path + ['net.ipv4.neigh.default.gc_thresh2', 'value', gc_thresh2]) + self.cli_set(path + ['net.ipv4.neigh.default.gc_thresh3', 'value', gc_thresh3]) - self.cli_set(base_path + ['performance', 'throughput']) + self.cli_set(base_path + ['performance', 'network-throughput']) self.cli_commit() self.assertTrue(is_systemd_service_active(tuned_service)) @@ -94,6 +100,60 @@ class TestSystemOption(VyOSUnitTestSHIM.TestCase): self.cli_commit() self.assertFalse(os.path.exists(ssh_client_opt_file)) + def test_kernel_options(self): + amd_pstate_mode = 'active' + isolate_cpus = '1,2,3' + nohz_full = '2' + rcu_no_cbs = '1,2,4-5' + default_hp_size = '2M' + hp_size_1g = '1G' + hp_size_2m = '2M' + hp_count_1g = '2' + hp_count_2m = '512' + + self.cli_set(['system', 'option', 'kernel', 'cpu', 'disable-nmi-watchdog']) + self.cli_set(['system', 'option', 'kernel', 'cpu', 'isolate-cpus', isolate_cpus]) + self.cli_set(['system', 'option', 'kernel', 'cpu', 'nohz-full', nohz_full]) + self.cli_set(['system', 'option', 'kernel', 'cpu', 'rcu-no-cbs', rcu_no_cbs]) + self.cli_set(['system', 'option', 'kernel', 'disable-hpet']) + self.cli_set(['system', 'option', 'kernel', 'disable-mce']) + self.cli_set(['system', 'option', 'kernel', 'disable-mitigations']) + self.cli_set(['system', 'option', 'kernel', 'disable-power-saving']) + self.cli_set(['system', 'option', 'kernel', 'disable-softlockup']) + self.cli_set(['system', 'option', 'kernel', 'memory', 'disable-numa-balancing']) + self.cli_set(['system', 'option', 'kernel', 'memory', 'default-hugepage-size', default_hp_size]) + self.cli_set(['system', 'option', 'kernel', 'memory', 'hugepage-size', hp_size_1g, 'hugepage-count', hp_count_1g]) + self.cli_set(['system', 'option', 'kernel', 'memory', 'hugepage-size', hp_size_2m, 'hugepage-count', hp_count_2m]) + self.cli_set(['system', 'option', 'kernel', 'quiet']) + + self.cli_set(['system', 'option', 'kernel', 'amd-pstate-driver', amd_pstate_mode]) + cpu_vendor = get_cpus()[0]['vendor_id'] + if cpu_vendor != 'AuthenticAMD': + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(['system', 'option', 'kernel', 'amd-pstate-driver']) + + self.cli_commit() + + # Read GRUB config file for current running image + tmp = read_file(f'{image.grub.GRUB_DIR_VYOS_VERS}/{image.get_running_image()}.cfg') + self.assertIn(' mitigations=off', tmp) + self.assertIn(' intel_idle.max_cstate=0 processor.max_cstate=1', tmp) + self.assertIn(' quiet', tmp) + self.assertIn(' nmi_watchdog=0', tmp) + self.assertIn(' hpet=disable', tmp) + self.assertIn(' mce=off', tmp) + self.assertIn(' nosoftlockup', tmp) + self.assertIn(f' isolcpus={isolate_cpus}', tmp) + self.assertIn(f' nohz_full={nohz_full}', tmp) + self.assertIn(f' rcu_nocbs={rcu_no_cbs}', tmp) + self.assertIn(f' default_hugepagesz={default_hp_size}', tmp) + self.assertIn(f' hugepagesz={hp_size_1g} hugepages={hp_count_1g}', tmp) + self.assertIn(f' hugepagesz={hp_size_2m} hugepages={hp_count_2m}', tmp) + self.assertIn(' numa_balancing=disable', tmp) + + if cpu_vendor == 'AuthenticAMD': + self.assertIn(f' initcall_blacklist=acpi_cpufreq_init amd_pstate={amd_pstate_mode}', tmp) if __name__ == '__main__': - unittest.main(verbosity=2, failfast=True) + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_system_sflow.py b/smoketest/scripts/cli/test_system_sflow.py index 74c065473..700253e2b 100755 --- a/smoketest/scripts/cli/test_system_sflow.py +++ b/smoketest/scripts/cli/test_system_sflow.py @@ -96,6 +96,39 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase): for interface in Section.interfaces('ethernet'): self.assertIn(f'pcap {{ dev={interface} }}', hsflowd) + def test_sflow_ipv6(self): + sampling_rate = '100' + default_polling = '30' + default_port = '6343' + sflow_server = { + '2001:db8::1': {}, + '2001:db8::2': {'port': '8023'}, + } + + for interface in Section.interfaces('ethernet'): + self.cli_set(base_path + ['interface', interface]) + + self.cli_set(base_path + ['sampling-rate', sampling_rate]) + for server, server_config in sflow_server.items(): + self.cli_set(base_path + ['server', server]) + if 'port' in server_config: + self.cli_set(base_path + ['server', server, 'port', server_config['port']]) + + # commit changes + self.cli_commit() + + # verify configuration + hsflowd = read_file(hsflowd_conf) + + self.assertIn(f'sampling={sampling_rate}', hsflowd) + self.assertIn(f'polling={default_polling}', hsflowd) + + for server, server_config in sflow_server.items(): + if 'port' in server_config: + self.assertIn(f'collector {{ ip = {server} udpport = {server_config["port"]} }}', hsflowd) + else: + self.assertIn(f'collector {{ ip = {server} udpport = {default_port} }}', hsflowd) + def test_vrf(self): interface = 'eth0' server = '192.0.2.1' diff --git a/smoketest/scripts/cli/test_system_syslog.py b/smoketest/scripts/cli/test_system_syslog.py index 45a5b4087..f3e1f65ea 100755 --- a/smoketest/scripts/cli/test_system_syslog.py +++ b/smoketest/scripts/cli/test_system_syslog.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright (C) 2019-2025 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 @@ -14,23 +14,33 @@ # 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 re +import os import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.configsession import ConfigSessionError from vyos.utils.file import read_file +from vyos.utils.process import cmd from vyos.utils.process import process_named_running +from vyos.xml_ref import default_value PROCESS_NAME = 'rsyslogd' -RSYSLOG_CONF = '/etc/rsyslog.d/00-vyos.conf' +RSYSLOG_CONF = '/run/rsyslog/rsyslog.conf' base_path = ['system', 'syslog'] -def get_config_value(key): - tmp = read_file(RSYSLOG_CONF) - tmp = re.findall(r'\n?{}\s+(.*)'.format(key), tmp) - return tmp[0] +dummy_interface = 'dum372874' + +def get_config(string=''): + """ + Retrieve current "running configuration" from FRR + string: search for a specific start string in the configuration + """ + command = 'cat /run/rsyslog/rsyslog.conf' + if string: + command += f' | sed -n "/^{string}$/,/}}/p"' # }} required to escape } in f-string + return cmd(command) class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): @classmethod @@ -40,6 +50,7 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + cls.cli_delete(cls, ['vrf']) def tearDown(self): # Check for running process @@ -49,31 +60,249 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() + # The default syslog implementation should make syslog.service a + # symlink to itself + self.assertEqual(os.readlink('/etc/systemd/system/syslog.service'), + '/lib/systemd/system/rsyslog.service') + # Check for running process self.assertFalse(process_named_running(PROCESS_NAME)) - def test_syslog_basic(self): - host1 = '127.0.0.10' - host2 = '127.0.0.20' + def test_console(self): + level = 'warning' + self.cli_set(base_path + ['console', 'facility', 'all', 'level'], value=level) + self.cli_commit() + + rsyslog_conf = get_config() + config = [ + f'if prifilt("*.{level}") then {{', # {{ required to escape { in f-string + 'action(type="omfile" file="/dev/console")', + ] + for tmp in config: + self.assertIn(tmp, rsyslog_conf) + + def test_basic(self): + hostname = 'vyos123' + domain_name = 'example.local' + default_marker_interval = default_value(base_path + ['marker', 'interval']) + + facility = { + 'auth': {'level': 'info'}, + 'kern': {'level': 'debug'}, + 'all': {'level': 'notice'}, + } - self.cli_set(base_path + ['host', host1, 'port', '999']) - self.cli_set(base_path + ['host', host1, 'facility', 'all', 'level', 'all']) - self.cli_set(base_path + ['host', host2, 'facility', 'kern', 'level', 'err']) - self.cli_set(base_path + ['console', 'facility', 'all', 'level', 'warning']) + self.cli_set(['system', 'host-name'], value=hostname) + self.cli_set(['system', 'domain-name'], value=domain_name) + self.cli_set(base_path + ['preserve-fqdn']) + for tmp, tmp_options in facility.items(): + level = tmp_options['level'] + self.cli_set(base_path + ['local', 'facility', tmp, 'level'], value=level) self.cli_commit() - # verify log level and facilities in config file - # *.warning /dev/console - # *.* @198.51.100.1:999 - # kern.err @192.0.2.1:514 - config = [get_config_value('\*.\*'), get_config_value('kern.err'), get_config_value('\*.warning')] - expected = [f'@{host1}:999', f'@{host2}:514', '/dev/console'] - - for i in range(0,3): - self.assertIn(expected[i], config[i]) - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + + config = get_config('') + expected = [ + f'module(load="immark" interval="{default_marker_interval}")', + 'global(preserveFQDN="on")', + f'global(localHostname="{hostname}.{domain_name}")', + ] + for e in expected: + self.assertIn(e, config) + + config = get_config('#### GLOBAL LOGGING ####') + prifilt = [] + for tmp, tmp_options in facility.items(): + if tmp == 'all': + tmp = '*' + level = tmp_options['level'] + prifilt.append(f'{tmp}.{level}') + + prifilt.sort() + prifilt = ','.join(prifilt) + + self.assertIn(f'if prifilt("{prifilt}") then {{', config) + self.assertIn( ' action(', config) + self.assertIn( ' type="omfile"', config) + self.assertIn( ' file="/var/log/messages"', config) + self.assertIn( ' rotation.sizeLimit="524288"', config) + self.assertIn( ' rotation.sizeLimitCommand="/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog"', config) + + self.cli_set(base_path + ['marker', 'disable']) + self.cli_commit() + + config = get_config('') + self.assertNotIn('module(load="immark"', config) + + def test_remote(self): + dummy_if_path = ['interfaces', 'dummy', dummy_interface] + rhosts = { + '169.254.0.1': { + 'facility': {'auth' : {'level': 'info'}}, + 'protocol': 'udp', + }, + '2001:db8::1': { + 'facility': {'all' : {'level': 'debug'}}, + 'port': '1514', + 'protocol': 'udp', + }, + 'syslog.vyos.net': { + 'facility': {'all' : {'level': 'debug'}}, + 'port': '1515', + 'protocol': 'tcp', + }, + '169.254.0.3': { + 'facility': {'auth' : {'level': 'info'}, + 'kern' : {'level': 'debug'}, + 'all' : {'level': 'notice'}, + }, + 'format': ['include-timezone', 'octet-counted'], + 'protocol': 'tcp', + 'port': '10514', + }, + } + default_port = default_value(base_path + ['remote', next(iter(rhosts)), 'port']) + default_protocol = default_value(base_path + ['remote', next(iter(rhosts)), 'protocol']) + + for remote, remote_options in rhosts.items(): + remote_base = base_path + ['remote', remote] + + if 'port' in remote_options: + self.cli_set(remote_base + ['port'], value=remote_options['port']) + + if 'facility' in remote_options: + for facility, facility_options in remote_options['facility'].items(): + level = facility_options['level'] + self.cli_set(remote_base + ['facility', facility, 'level'], + value=level) + + if 'format' in remote_options: + for format in remote_options['format']: + self.cli_set(remote_base + ['format'], value=format) + + if 'protocol' in remote_options: + protocol = remote_options['protocol'] + self.cli_set(remote_base + ['protocol'], value=protocol) + + if 'source_address' in remote_options: + source_address = remote_options['source_address'] + self.cli_set(remote_base + ['source-address', source_address]) + + # check validate() - source address does not exist + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(dummy_if_path + ['address', f'{source_address}/32']) + + self.cli_commit() + + config = read_file(RSYSLOG_CONF) + for remote, remote_options in rhosts.items(): + config = get_config(f'# Remote syslog to {remote}') + prifilt = [] + if 'facility' in remote_options: + for facility, facility_options in remote_options['facility'].items(): + level = facility_options['level'] + if facility == 'all': + facility = '*' + prifilt.append(f'{facility}.{level}') + + prifilt.sort() + prifilt = ','.join(prifilt) + if not prifilt: + # Skip test - as we do not render anything if no facility is set + continue + + self.assertIn(f'if prifilt("{prifilt}") then {{', config) + self.assertIn( ' type="omfwd"', config) + self.assertIn(f' target="{remote}"', config) + + port = default_port + if 'port' in remote_options: + port = remote_options['port'] + self.assertIn(f'port="{port}"', config) + + protocol = default_protocol + if 'protocol' in remote_options: + protocol = remote_options['protocol'] + self.assertIn(f'protocol="{protocol}"', config) + + if 'format' in remote_options: + if 'include-timezone' in remote_options['format']: + self.assertIn( ' template="RSYSLOG_SyslogProtocol23Format"', config) + + if 'octet-counted' in remote_options['format']: + self.assertIn( ' TCP_Framing="octet-counted"', config) + else: + self.assertIn( ' TCP_Framing="traditional"', config) + + # cleanup dummy interface + self.cli_delete(dummy_if_path) + + def test_vrf_source_address(self): + rhosts = { + '169.254.0.10': { }, + '169.254.0.11': { + 'vrf': {'name' : 'red', 'table' : '12321'}, + 'source_address' : '169.254.0.11', + }, + '169.254.0.12': { + 'vrf': {'name' : 'green', 'table' : '12322'}, + 'source_address' : '169.254.0.12', + }, + '169.254.0.13': { + 'vrf': {'name' : 'blue', 'table' : '12323'}, + 'source_address' : '169.254.0.13', + }, + } + + for remote, remote_options in rhosts.items(): + remote_base = base_path + ['remote', remote] + self.cli_set(remote_base + ['facility', 'all']) + + vrf = None + if 'vrf' in remote_options: + vrf = remote_options['vrf']['name'] + self.cli_set(['vrf', 'name', vrf, 'table'], + value=remote_options['vrf']['table']) + self.cli_set(remote_base + ['vrf'], value=vrf) + + if 'source_address' in remote_options: + source_address = remote_options['source_address'] + self.cli_set(remote_base + ['source-address'], + value=source_address) + + idx = source_address.split('.')[-1] + self.cli_set(['interfaces', 'dummy', f'dum{idx}', 'address'], + value=f'{source_address}/32') + if vrf: + self.cli_set(['interfaces', 'dummy', f'dum{idx}', 'vrf'], + value=vrf) + + self.cli_commit() + + for remote, remote_options in rhosts.items(): + config = get_config(f'# Remote syslog to {remote}') + + self.assertIn(f'target="{remote}"', config) + if 'vrf' in remote_options: + vrf = remote_options['vrf']['name'] + self.assertIn(f'Device="{vrf}"', config) + + if 'source_address' in remote_options: + source_address = remote_options['source_address'] + self.assertIn(f'Address="{source_address}"', config) + + # Cleanup VRF/Dummy interfaces + for remote, remote_options in rhosts.items(): + if 'vrf' in remote_options: + vrf = remote_options['vrf']['name'] + self.cli_delete(['vrf', 'name', vrf]) + + if 'source_address' in remote_options: + source_address = remote_options['source_address'] + idx = source_address.split('.')[-1] + self.cli_delete(['interfaces', 'dummy', f'dum{idx}']) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py index 3b8687b93..c1d943bde 100755 --- a/smoketest/scripts/cli/test_vpn_ipsec.py +++ b/smoketest/scripts/cli/test_vpn_ipsec.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright (C) 2021-2025 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 @@ -21,6 +21,7 @@ from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.ifconfig import Interface +from vyos.utils.convert import encode_to_base64 from vyos.utils.process import process_named_running from vyos.utils.file import read_file @@ -351,25 +352,129 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.tearDownPKI() + def test_site_to_site_vti_ts_afi(self): + local_address = '192.0.2.10' + vti = 'vti10' + # IKE + self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2']) + self.cli_set(base_path + ['ike-group', ike_group, 'disable-mobike']) + # ESP + self.cli_set(base_path + ['esp-group', esp_group, 'compression']) + # VTI interface + self.cli_set(vti_path + [vti, 'address', '10.1.1.1/24']) + + # vpn ipsec auth psk <tag> id <x.x.x.x> + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_id]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret]) + + # Site to site + peer_base_path = base_path + ['site-to-site', 'peer', connection_name] + self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret']) + self.cli_set(peer_base_path + ['connection-type', 'none']) + self.cli_set(peer_base_path + ['force-udp-encapsulation']) + self.cli_set(peer_base_path + ['ike-group', ike_group]) + self.cli_set(peer_base_path + ['default-esp-group', esp_group]) + self.cli_set(peer_base_path + ['local-address', local_address]) + self.cli_set(peer_base_path + ['remote-address', peer_ip]) + self.cli_set(peer_base_path + ['vti', 'bind', vti]) + self.cli_set(peer_base_path + ['vti', 'esp-group', esp_group]) + self.cli_set(peer_base_path + ['vti', 'traffic-selector', 'local', 'prefix', '0.0.0.0/0']) + self.cli_set(peer_base_path + ['vti', 'traffic-selector', 'remote', 'prefix', '192.0.2.1/32']) + self.cli_set(peer_base_path + ['vti', 'traffic-selector', 'remote', 'prefix', '192.0.2.3/32']) + + self.cli_commit() + + swanctl_conf = read_file(swanctl_file) + if_id = vti.lstrip('vti') + # The key defaults to 0 and will match any policies which similarly do + # not have a lookup key configuration - thus we shift the key by one + # to also support a vti0 interface + if_id = str(int(if_id) +1) + swanctl_conf_lines = [ + f'version = 2', + f'auth = psk', + f'proposals = aes128-sha1-modp1024', + f'esp_proposals = aes128-sha1-modp1024', + f'local_addrs = {local_address} # dhcp:no', + f'mobike = no', + f'remote_addrs = {peer_ip}', + f'mode = tunnel', + f'local_ts = 0.0.0.0/0', + f'remote_ts = 192.0.2.1/32,192.0.2.3/32', + f'ipcomp = yes', + f'start_action = none', + f'replay_window = 32', + f'if_id_in = {if_id}', # will be 11 for vti10 - shifted by one + f'if_id_out = {if_id}', + f'updown = "/etc/ipsec.d/vti-up-down {vti}"' + ] + for line in swanctl_conf_lines: + self.assertIn(line, swanctl_conf) + + # Check IPv6 TS + self.cli_delete(peer_base_path + ['vti', 'traffic-selector']) + self.cli_set(peer_base_path + ['vti', 'traffic-selector', 'local', 'prefix', '::/0']) + self.cli_set(peer_base_path + ['vti', 'traffic-selector', 'remote', 'prefix', '::/0']) + self.cli_commit() + swanctl_conf = read_file(swanctl_file) + swanctl_conf_lines = [ + f'local_ts = ::/0', + f'remote_ts = ::/0', + f'updown = "/etc/ipsec.d/vti-up-down {vti}"' + ] + for line in swanctl_conf_lines: + self.assertIn(line, swanctl_conf) + + # Check both TS (IPv4 + IPv6) + self.cli_delete(peer_base_path + ['vti', 'traffic-selector']) + self.cli_commit() + swanctl_conf = read_file(swanctl_file) + swanctl_conf_lines = [ + f'local_ts = 0.0.0.0/0,::/0', + f'remote_ts = 0.0.0.0/0,::/0', + f'updown = "/etc/ipsec.d/vti-up-down {vti}"' + ] + for line in swanctl_conf_lines: + self.assertIn(line, swanctl_conf) + + def test_dmvpn(self): - tunnel_if = 'tun100' - nhrp_secret = 'secret' ike_lifetime = '3600' esp_lifetime = '1800' + tunnel_if = "tun100" + tunnel_ip = '172.16.253.134/32' + tunnel_source = "192.0.2.134" + tunnel_encapsulation = "gre" + esp_group = "ESP-HUB" + ike_group = "IKE-HUB" + nhrp_secret = "vyos123" + nhrp_holdtime = '300' + nhs_tunnelip = '172.16.253.1' + nhs_nbmaip = '192.0.2.1' + map_tunnelip = '172.16.253.135' + map_nbmaip = "192.0.2.135" + nhrp_networkid = '1' + # Tunnel - self.cli_set(tunnel_path + [tunnel_if, 'address', '172.16.253.134/29']) - self.cli_set(tunnel_path + [tunnel_if, 'encapsulation', 'gre']) - self.cli_set(tunnel_path + [tunnel_if, 'source-address', '192.0.2.1']) - self.cli_set(tunnel_path + [tunnel_if, 'enable-multicast']) - self.cli_set(tunnel_path + [tunnel_if, 'parameters', 'ip', 'key', '1']) + self.cli_set(tunnel_path + [tunnel_if, "address", tunnel_ip]) + self.cli_set(tunnel_path + [tunnel_if, "encapsulation", tunnel_encapsulation]) + self.cli_set(tunnel_path + [tunnel_if, "source-address", tunnel_source]) + self.cli_set(tunnel_path + [tunnel_if, "enable-multicast"]) + self.cli_set(tunnel_path + [tunnel_if, "parameters", "ip", "key", "1"]) # NHRP - self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'cisco-authentication', nhrp_secret]) - self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'holding-time', '300']) - self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'multicast', 'dynamic']) - self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'redirect']) - self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'shortcut']) + self.cli_set(nhrp_path + ["tunnel", tunnel_if, "authentication", nhrp_secret]) + self.cli_set(nhrp_path + ["tunnel", tunnel_if, "holdtime", nhrp_holdtime]) + self.cli_set(nhrp_path + ["tunnel", tunnel_if, "multicast", nhs_tunnelip]) + self.cli_set(nhrp_path + ["tunnel", tunnel_if, "redirect"]) + self.cli_set(nhrp_path + ["tunnel", tunnel_if, "shortcut"]) + self.cli_set(nhrp_path + ["tunnel", tunnel_if, "registration-no-unique"]) + self.cli_set(nhrp_path + ["tunnel", tunnel_if, "network-id", nhrp_networkid]) + self.cli_set(nhrp_path + ["tunnel", tunnel_if, "nhs", "tunnel-ip", nhs_tunnelip, "nbma", nhs_nbmaip]) + self.cli_set(nhrp_path + ["tunnel", tunnel_if, "map", "tunnel-ip", map_tunnelip, "nbma", map_nbmaip]) # IKE/ESP Groups self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', esp_lifetime]) @@ -398,11 +503,11 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): swanctl_conf = read_file(swanctl_file) swanctl_lines = [ - f'proposals = aes128-sha1-modp1024,aes256-sha1-prfsha1-modp1024', + f'proposals = aes256-sha1-prfsha1-modp1024', f'version = 1', f'rekey_time = {ike_lifetime}s', f'rekey_time = {esp_lifetime}s', - f'esp_proposals = aes128-sha1-modp1024,aes256-sha1-modp1024,3des-md5-modp1024', + f'esp_proposals = aes256-sha1-modp1024,3des-md5-modp1024', f'local_ts = dynamic[gre]', f'remote_ts = dynamic[gre]', f'mode = transport', @@ -495,6 +600,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): local_id = 'vyos-r1' remote_id = 'vyos-r2' peer_base_path = base_path + ['site-to-site', 'peer', connection_name] + secret_base64 = encode_to_base64(secret) self.cli_set(tunnel_path + ['tun1', 'encapsulation', 'gre']) self.cli_set(tunnel_path + ['tun1', 'source-address', local_address]) @@ -509,7 +615,8 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id]) self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_address]) self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip]) - self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret_base64]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret-type', 'base64']) self.cli_set(peer_base_path + ['authentication', 'local-id', local_id]) self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret']) @@ -546,7 +653,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): f'id-{regex_uuid4} = "{remote_id}"', f'id-{regex_uuid4} = "{peer_ip}"', f'id-{regex_uuid4} = "{local_address}"', - f'secret = "{secret}"', + f'secret = 0s{secret_base64}', ] for line in swanctl_secrets_lines: @@ -947,7 +1054,8 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', ike_lifetime]) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'dh-group', '14']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'encryption', 'aes256']) - self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha512']) + # a hash algorithm that cannot be mapped to an equivalent PRF + self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'aes192gmac']) # ESP self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', eap_lifetime]) @@ -968,6 +1076,11 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'name-server', name_server]) self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'prefix', prefix]) + # verify() - IKE group use not mapped hash algorithm + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha512']) self.cli_commit() self.assertTrue(os.path.exists(dhcp_interfaces_file)) diff --git a/smoketest/scripts/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py index 2bb6c91c1..30980f9ec 100755 --- a/smoketest/scripts/cli/test_vrf.py +++ b/smoketest/scripts/cli/test_vrf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright (C) 2020-2025 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 @@ -18,10 +18,12 @@ import re import os import unittest -from base_vyostest_shim import VyOSUnitTestSHIM from json import loads from jmespath import search +from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME + from vyos.configsession import ConfigSessionError from vyos.ifconfig import Interface from vyos.ifconfig import Section @@ -51,6 +53,10 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): else: for tmp in Section.interfaces('ethernet', vlan=False): cls._interfaces.append(tmp) + + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME + # call base-classes classmethod super(VRFTest, cls).setUpClass() @@ -112,7 +118,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): regex = f'{table}\s+{vrf}\s+#\s+{description}' self.assertTrue(re.findall(regex, iproute2_config)) - frrconfig = self.getFRRconfig(f'vrf {vrf}') + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) self.assertEqual(int(table), get_vrf_tableid(vrf)) @@ -233,7 +239,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): self.assertTrue(interface_exists(vrf)) - frrconfig = self.getFRRconfig(f'vrf {vrf}') + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) self.assertIn(f' ip route {prefix} {next_hop}', frrconfig) @@ -317,7 +323,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Verify route-map properly applied to FRR for vrf in vrfs: - frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon='zebra') + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertIn(f'vrf {vrf}', frrconfig) for protocol in v4_protocols: self.assertIn(f' ip protocol {protocol} route-map route-map-{vrf}-{protocol}', frrconfig) @@ -332,8 +338,8 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Verify route-map properly is removed from FRR for vrf in vrfs: - frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon='zebra') - self.assertNotIn(f'vrf {vrf}', frrconfig) + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') + self.assertNotIn(f' ip protocol', frrconfig) def test_vrf_ip_ipv6_protocol_non_existing_route_map(self): table = '6100' @@ -380,7 +386,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Verify route-map properly applied to FRR for vrf in vrfs: - frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon='zebra') + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertIn(f'vrf {vrf}', frrconfig) for protocol in v6_protocols: # VyOS and FRR use a different name for OSPFv3 (IPv6) @@ -399,8 +405,8 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Verify route-map properly is removed from FRR for vrf in vrfs: - frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon='zebra') - self.assertNotIn(f'vrf {vrf}', frrconfig) + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') + self.assertNotIn(f' ipv6 protocol', frrconfig) def test_vrf_vni_duplicates(self): base_table = '6300' @@ -429,7 +435,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): for vrf in vrfs: self.assertTrue(interface_exists(vrf)) - frrconfig = self.getFRRconfig(f'vrf {vrf}') + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) # Increment table ID for the next run table = str(int(table) + 1) @@ -451,7 +457,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): for vrf in vrfs: self.assertTrue(interface_exists(vrf)) - frrconfig = self.getFRRconfig(f'vrf {vrf}') + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) # Increment table ID for the next run table = str(int(table) + 1) @@ -474,7 +480,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): for vrf in vrfs: self.assertTrue(interface_exists(vrf)) - frrconfig = self.getFRRconfig(f'vrf {vrf}') + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) # Increment table ID for the next run table = str(int(table) + 2) @@ -494,7 +500,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): for vrf in vrfs: self.assertTrue(interface_exists(vrf)) - frrconfig = self.getFRRconfig(f'vrf {vrf}') + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) # Increment table ID for the next run table = str(int(table) + 2) @@ -502,7 +508,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Verify purple VRF/VNI self.assertTrue(interface_exists(purple)) table = str(int(table) + 10) - frrconfig = self.getFRRconfig(f'vrf {purple}') + frrconfig = self.getFRRconfig(f'vrf {purple}', endsection='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) # Now delete all the VNIs @@ -517,12 +523,12 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): for vrf in vrfs: self.assertTrue(interface_exists(vrf)) - frrconfig = self.getFRRconfig(f'vrf {vrf}') + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertNotIn('vni', frrconfig) # Verify purple VNI remains self.assertTrue(interface_exists(purple)) - frrconfig = self.getFRRconfig(f'vrf {purple}') + frrconfig = self.getFRRconfig(f'vrf {purple}', endsection='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) def test_vrf_ip_ipv6_nht(self): @@ -540,7 +546,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Verify route-map properly applied to FRR for vrf in vrfs: - frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon='zebra') + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertIn(f'vrf {vrf}', frrconfig) self.assertIn(f' no ip nht resolve-via-default', frrconfig) self.assertIn(f' no ipv6 nht resolve-via-default', frrconfig) @@ -555,7 +561,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Verify route-map properly is removed from FRR for vrf in vrfs: - frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon='zebra') + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertNotIn(f' no ip nht resolve-via-default', frrconfig) self.assertNotIn(f' no ipv6 nht resolve-via-default', frrconfig) |