diff options
Diffstat (limited to 'smoketest/scripts/cli')
113 files changed, 10575 insertions, 1577 deletions
diff --git a/smoketest/scripts/cli/base_accel_ppp_test.py b/smoketest/scripts/cli/base_accel_ppp_test.py index 750702e98..b4e52004e 100644 --- a/smoketest/scripts/cli/base_accel_ppp_test.py +++ b/smoketest/scripts/cli/base_accel_ppp_test.py @@ -1,4 +1,4 @@ -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -26,6 +26,11 @@ from vyos.utils.process import cmd class BasicAccelPPPTest: class TestCase(VyOSUnitTestSHIM.TestCase): + _base_path = None + _config_file = None + _chap_secrets = None + _protocol_section = None + @classmethod def setUpClass(cls): cls._process_name = "accel-pppd" @@ -41,6 +46,8 @@ class BasicAccelPPPTest: # ensure we can also run this test on a live system - so lets clean # out the current configuration :) self.cli_delete(self._base_path) + # always forward to base class + super().setUp() def tearDown(self): # Check for running process @@ -51,6 +58,8 @@ class BasicAccelPPPTest: # Check for running process self.assertFalse(process_named_running(self._process_name)) + # always forward to base class + super().tearDown() def set(self, path): self.cli_set(self._base_path + path) @@ -61,7 +70,7 @@ class BasicAccelPPPTest: def basic_protocol_specific_config(self): """ An astract method. - Initialize protocol scpecific configureations. + Initialize protocol specific configurations. """ self.assertFalse(True, msg="Function must be defined") @@ -117,7 +126,7 @@ class BasicAccelPPPTest: """ Return part of configuration from line where the first injection of start keyword to the line - where the first injection of end keyowrd + where the first injection of end keyword :param start: start keyword :type start: str :param end: end keyword diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index a9b758802..50606efe3 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -1,4 +1,4 @@ -# Copyright (C) 2019-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -13,14 +13,15 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import re +import jmespath -from netifaces import AF_INET -from netifaces import AF_INET6 -from netifaces import ifaddresses +from json import loads +from netifaces import ifaddresses # pylint: disable = no-name-in-module +from socket import AF_INET +from socket import AF_INET6 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 @@ -46,6 +47,8 @@ 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 @@ -119,9 +122,9 @@ def get_certificate_count(interface, cert_type): tmp = read_file(f'/run/wpa_supplicant/{interface}_{cert_type}.pem') return tmp.count(CERT_BEGIN) -def is_mirrored_to(interface, mirror_if, qdisc): +def is_mirrored_to(interface, mirror_if, qdisc) -> bool: """ - Ask TC if we are mirroring traffic to a discrete interface. + Ask tc(8) if we are mirroring traffic to a specific interface. interface: source interface mirror_if: destination where we mirror our data to @@ -130,12 +133,12 @@ def is_mirrored_to(interface, mirror_if, qdisc): if qdisc not in ['ffff', '1']: raise ValueError() - ret_val = False - tmp = cmd(f'tc -s -p filter ls dev {interface} parent {qdisc}: | grep mirred') - tmp = tmp.lower() - if mirror_if in tmp: - ret_val = True - return ret_val + tmp = loads(cmd(f'tc -json filter ls dev {interface} parent {qdisc}:')) + # the following syntax looks odd but we need to filter out the first + # result sets from tc which do not have "options.actions...". + tmp = jmespath.search("[?options.actions[0].kind=='mirred'].options.actions[0].{mirred_action: mirred_action, to_dev: to_dev} | [0]", tmp) + return bool(dict_search('mirred_action', tmp) == 'mirror' and dict_search('to_dev', tmp) == mirror_if) + class BasicInterfaceTest: class TestCase(VyOSUnitTestSHIM.TestCase): _test_dhcp = False @@ -158,7 +161,7 @@ class BasicInterfaceTest: _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'] - _mirror_interfaces = [] + _mirror_interfaces = ['dum21354'] # choose IPv6 minimum MTU value for tests - this must always work _mtu = '1280' @@ -178,15 +181,13 @@ class BasicInterfaceTest: cls._test_ipv6_pd = cli_defined(cls._base_path + ['dhcpv6-options'], 'pd') cls._test_mtu = cli_defined(cls._base_path, 'mtu') cls._test_vrf = cli_defined(cls._base_path, 'vrf') + cls._test_mirror = cli_defined(cls._base_path, 'mirror') # Setup mirror interfaces for SPAN (Switch Port Analyzer) for span in cls._mirror_interfaces: 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) @@ -217,9 +218,12 @@ class BasicInterfaceTest: else: self.assertFalse(process_named_running(daemon)) + # always forward to base class + super().tearDown() + 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 @@ -242,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' @@ -282,7 +286,7 @@ 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']) @@ -339,7 +343,7 @@ class BasicInterfaceTest: 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']) @@ -391,7 +395,7 @@ class BasicInterfaceTest: 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' @@ -436,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: @@ -453,8 +457,12 @@ class BasicInterfaceTest: self.cli_set(self._base_path + [interface, 'description', 'test_add_to_invalid_vrf']) def test_span_mirror(self): - if not self._mirror_interfaces: - self.skipTest('not supported') + if not self._test_mirror: + self.skipTest(MSG_TESTCASE_UNSUPPORTED) + + for interface in self._interfaces: + for option in self._options.get(interface, []): + self.cli_set(self._base_path + [interface] + option.split()) # Check the two-way mirror rules of ingress and egress for mirror in self._mirror_interfaces: @@ -470,6 +478,18 @@ class BasicInterfaceTest: self.assertTrue(is_mirrored_to(interface, mirror, 'ffff')) self.assertTrue(is_mirrored_to(interface, mirror, '1')) + # delete interface mirror - check that configuration from tc is removed + for mirror in self._mirror_interfaces: + for interface in self._interfaces: + self.cli_delete(self._base_path + [interface, 'mirror']) + self.cli_commit() + + # Verify config + for mirror in self._mirror_interfaces: + for interface in self._interfaces: + self.assertFalse(is_mirrored_to(interface, mirror, 'ffff')) + self.assertFalse(is_mirrored_to(interface, mirror, '1')) + def test_interface_disable(self): # Check if description can be added to interface and # can be read back @@ -561,9 +581,9 @@ class BasicInterfaceTest: self.assertTrue(is_intf_addr_assigned(intf, addr['addr'])) def test_ipv6_link_local_address(self): - # Common function for IPv6 link-local address assignemnts + # Common function for IPv6 link-local address assignments if not self._test_ipv6: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) for interface in self._interfaces: base = self._base_path + [interface] @@ -594,7 +614,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] @@ -614,7 +634,7 @@ class BasicInterfaceTest: # Testcase if MTU can be changed to 1200 on non IPv6 # enabled interfaces if not self._test_mtu or not self._test_ipv6: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) old_mtu = self._mtu self._mtu = '1200' @@ -650,7 +670,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] @@ -695,24 +715,27 @@ 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' + # Some drivers are limited to an MTU of 1500 byte - e.g. when VyOS + # runs on PROXMOX with default bridge settings. We use lower values + # for this test which will fit for almost every NIC + mtu_1300 = '1300' + mtu_1420 = '1420' for interface in self._interfaces: base = self._base_path + [interface] - self.cli_set(base + ['mtu', mtu_1500]) + self.cli_set(base + ['mtu', mtu_1300]) for option in self._options.get(interface, []): self.cli_set(base + option.split()) if 'source-interface' in option: iface = option.split()[-1] iface_type = Section.section(iface) - self.cli_set(['interfaces', iface_type, iface, 'mtu', mtu_9000]) + self.cli_set(['interfaces', iface_type, iface, 'mtu', mtu_1420]) for vlan in self._vlan_range: base = self._base_path + [interface, 'vif', vlan] - self.cli_set(base + ['mtu', mtu_9000]) + self.cli_set(base + ['mtu', mtu_1420]) # check validate() - Interface MTU "9000" too high, parent interface MTU is "1500"! with self.assertRaises(ConfigSessionError): @@ -721,18 +744,18 @@ class BasicInterfaceTest: # Change MTU on base interface to be the same as on the VIF interface for interface in self._interfaces: base = self._base_path + [interface] - self.cli_set(base + ['mtu', mtu_9000]) + self.cli_set(base + ['mtu', mtu_1420]) self.cli_commit() # Verify MTU on base and VIF interfaces for interface in self._interfaces: tmp = get_interface_config(interface) - self.assertEqual(tmp['mtu'], int(mtu_9000)) + self.assertEqual(tmp['mtu'], int(mtu_1420)) for vlan in self._vlan_range: tmp = get_interface_config(f'{interface}.{vlan}') - self.assertEqual(tmp['mtu'], int(mtu_9000)) + self.assertEqual(tmp['mtu'], int(mtu_1420)) def test_vif_8021q_qos_change(self): @@ -741,7 +764,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] @@ -811,7 +834,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] @@ -851,7 +874,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] @@ -918,7 +941,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] @@ -956,7 +979,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' @@ -1058,12 +1081,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] @@ -1086,6 +1110,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: @@ -1117,9 +1144,16 @@ 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: @@ -1170,7 +1204,7 @@ class BasicInterfaceTest: 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)) @@ -1213,7 +1247,6 @@ class BasicInterfaceTest: self.assertIn(f'prefix-interface {delegatee}' + r' {', dhcpc6_config) self.assertIn(f'ifid {address};', dhcpc6_config) self.assertIn(f'sla-id {sla_id};', dhcpc6_config) - self.assertIn(f'sla-len {sla_len};', dhcpc6_config) # increment sla-id sla_id = str(int(sla_id) + 1) @@ -1231,7 +1264,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)) @@ -1279,7 +1312,6 @@ class BasicInterfaceTest: self.assertIn(f'prefix-interface {delegatee}' + r' {', dhcpc6_config) self.assertIn(f'ifid {address};', dhcpc6_config) self.assertIn(f'sla-id {sla_id};', dhcpc6_config) - self.assertIn(f'sla-len {sla_len};', dhcpc6_config) # increment sla-id sla_id = str(int(sla_id) + 1) @@ -1297,7 +1329,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 f0674f187..590670a06 100644 --- a/smoketest/scripts/cli/base_vyostest_shim.py +++ b/smoketest/scripts/cli/base_vyostest_shim.py @@ -1,4 +1,4 @@ -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -13,30 +13,26 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import unittest import paramiko import pprint +import re +import unittest +from math import ceil from time import sleep from typing import Type +from vyos import ConfigError from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError -from vyos import ConfigError from vyos.defaults import commit_lock +from vyos.frrender import mgmt_daemon from vyos.utils.process import cmd +from vyos.utils.process import process_named_running 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. @@ -44,36 +40,51 @@ CSTORE_GUARD_TIME = 4 # Using this approach we can not render a live system useless while running any # kind of smoketest. In addition it adds debug capabilities like printing the # command used to execute the test. + class VyOSUnitTestSHIM: class TestCase(unittest.TestCase): - # if enabled in derived class, print out each and every set/del command - # on the CLI. This is usefull to grap all the commands required to - # trigger the certain failure condition. - # Use "self.debug = True" in derived classes setUp() method + # If enabled, print out each and every set/del command on stdout. + # This is useful to grab all the commands required to trigger the + # certain failure condition. 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 + mgmt_daemon_pid = 0 + + @staticmethod + def debug_on(): + return os.path.exists('/tmp/vyos.smoketest.debug') + @classmethod def setUpClass(cls): cls._session = ConfigSession(os.getpid()) cls._session.save_config(save_config) - if os.path.exists('/tmp/vyos.smoketest.debug'): - cls.debug = True - pass + cls.debug = cls.debug_on() + + # Retrieve FRR mgmtd daemon PID - it is not allowed to crash, thus + # PID must remain the same + cls.mgmt_daemon_pid = process_named_running(mgmt_daemon) @classmethod def tearDownClass(cls): - # discard any pending changes which might caused a messed up config - cls._session.discard() - # ... and restore the initial state - cls._session.migrate_and_load_config(save_config) - try: + # commit pending changes done by derived tearDownClass() + # implementations like CLI cleanup cls._session.commit() except (ConfigError, ConfigSessionError): + # discard any pending changes which might have failed, causing a + # messed up config cls._session.discard() cls.fail(cls) + finally: + # restore previous configuration before the test + cls._session.migrate_and_load_config(save_config) + cls._session.commit() + + def setUp(self): + pass + + def tearDown(self): + # check process health and continuity + self.assertEqual(self.mgmt_daemon_pid, process_named_running(mgmt_daemon)) def cli_set(self, path, value=None): if self.debug: @@ -100,11 +111,12 @@ class VyOSUnitTestSHIM: 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 self._session.commit() - return out + def cli_save(self, file): + if self.debug: + print('save') + self._session.save_config(file) def op_mode(self, path : list) -> None: """ @@ -119,45 +131,79 @@ class VyOSUnitTestSHIM: pprint.pprint(out) return out - def getFRRconfig(self, string=None, end='$', endsection='^!', - substring=None, endsubsection=None, empty_retry=0): + def getFRRconfig(self, start_section:str=None, end_marker='$', stop_section='^!', + start_subsection:str=None, stop_subsection='^ exit') -> str: """ 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") + start_section: search for a specific start string in the configuration + end_marker: override default "line end $" marker to match on an + "open end" string + stop_section: end of the configuration + start_subsection: search section under the result found by string + stop_subsection: 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) + from vyos.utils.process import rc_cmd + + rc, frr_config = rc_cmd('vtysh -c "show running-config no-header"') + self.assertEqual(rc, 0) + + if not start_section: + return frr_config + + extracted = [] + in_section = False + for line in frr_config.splitlines(): + if not in_section: + if re.match(f'^{start_section}{end_marker}', line): + in_section = True + extracted.append(line) + else: + extracted.append(line) + if re.match(stop_section, line): + break + output = '\n'.join(extracted) + + # Use extracted list when searching for optional subsection + # used by e.g. BGP address-family check + if start_subsection: + extracted_subsection = [] + in_subsection = False + for line in extracted: + if not in_subsection: + if re.match(start_subsection, line): + in_subsection = True + extracted_subsection.append(line) + else: + extracted_subsection.append(line) + if re.match(stop_subsection, line): + break + output = '\n'.join(extracted_subsection) + + if self.debug: + print(output) + return output + + def getFRRopmode(self, command : str, json : bool=False): + from json import loads + if json: command += f' json' + out = cmd(f'vtysh -c "{command}"') + if json: + out = loads(out) 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 - def ssh_send_cmd(command, username, password, hostname='localhost'): + def ssh_send_cmd(command, username, password, key_filename=None, + hostname='localhost'): """ SSH command execution helper """ # Try to login via SSH ssh_client = paramiko.SSHClient() ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - ssh_client.connect(hostname=hostname, username=username, password=password) + ssh_client.connect(hostname=hostname, username=username, + password=password, key_filename=key_filename) _, stdout, stderr = ssh_client.exec_command(command) output = stdout.read().decode().strip() error = stderr.read().decode().strip() @@ -165,7 +211,64 @@ class VyOSUnitTestSHIM: return output, error # Verify nftables output - def verify_nftables(self, nftables_search, table, inverse=False, args=''): + def verify_nftables(self, nftables_search: list[list[str]], table: str, inverse: bool=False, args: str='') -> None: + """ + Assert presence or absence of lines in `nft list table` output. + + This helper inspects the output of `sudo nft {args} list table {table}` + and, for each entry in `nftables_search`, checks whether there exists + a single line that contains all specified substrings. + + #### Usage: + nftables output excerpt: + ```text + chain VYOS_STATE_POLICY { + ct state established counter packets 0 bytes 0 accept + ct state invalid counter packets 0 bytes 0 drop + ct state related counter packets 0 bytes 0 accept + } + ``` + + ##### Example 1: + Verify that the chain VYOS_STATE_POLICY exists and contains the specified fragments + + Code usage: + ```python + nftables_search = [ + ["chain VYOS_STATE_POLICY"], + ["ct state established", "accept"], + ] + self.verify_nftables(nftables_search, "ip vyos_filter") + ``` + + ##### Example 2 (inverse matching): + Verify that the ct state established does not have a verdict of drop + + Code usage: + ```python + nftables_search = [ + ["ct state established", "drop"] + ] + self.verify_nftables(nftables_search, "ip vyos_filter", inverse=True) + ``` + + Parameters: + nftables_search: list[list[str]] + A list of search groups. Each inner list contains substrings + that must all appear within the same output line to count as + a match. + table: str + Table spec accepted by nft (e.g. "ip vyos_filter" or + "ip6 vyos_filter"). + inverse: bool + If True, assert that no output line matches any search group. + If False, assert that each search group is matched at least once. + args: str + Extra flags for `nft` (e.g. "-a" to show rule handles or "-s" to omit counter hits). + + Raises: + AssertionError: If expectations are not met. + """ nftables_output = cmd(f'sudo nft {args} list table {table}') for search in nftables_search: @@ -176,7 +279,69 @@ class VyOSUnitTestSHIM: break self.assertTrue(not matched if inverse else matched, msg=search) - def verify_nftables_chain(self, nftables_search, table, chain, inverse=False, args=''): + def verify_nftables_chain(self, nftables_search: list[list[str]], table: str, chain: str, inverse: bool=False, args: str='') -> None: + """ + Assert presence or absence of lines in `nft list chain` output. + + This behaves like `verify_nftables` but focuses on a specific chain within a table using + `sudo nft {args} list chain {table} {chain}`. For each entry in `nftables_search`, it + checks whether there exists a single line that contains all specified substrings. + + #### Usage: + nftables output excerpt: + ```text + chain VYOS_INPUT_filter { + tcp dport 22 counter packets 0 bytes 0 accept + tcp dport 23 counter packets 0 bytes 0 drop + } + ``` + + ##### Example 1: + Verify the chain contains the specified fragments + + Code usage: + ```python + nftables_search = [ + ["tcp dport 22", "accept"], + ["tcp dport 23", "drop"] + ] + self.verify_nftables_chain( + nftables_search, table="ip vyos_filter", chain="VYOS_INPUT_filter" + ) + ``` + + ##### Example 2 (inverse matching): + Verify that a drop rule for tcp dport 22 is not present + + Code usage: + ```python + nftables_search = [ + ["tcp dport 22", "drop"] + ] + self.verify_nftables_chain( + nftables_search, table="ip vyos_filter", chain="VYOS_INPUT_filter", inverse=True + ) + ``` + + Parameters: + nftables_search: list[list[str]] + A list of search groups. Each inner list contains substrings + that must all appear within the same output line to count as + a match. + table: str + Table spec accepted by nft (e.g. "ip vyos_filter" or + "ip6 vyos_filter"). + chain: str + Chain name within the specified table. + inverse: bool + If True, assert that no output line matches any search group. + If False, assert that each search group is matched at least once. + args: str + Extra flags for `nft` (e.g. "-a" to show rule handles or "-s" to omit counter hits). + + Raises: + AssertionError: If expectations are not met. + """ nftables_output = cmd(f'sudo nft {args} list chain {table} {chain}') for search in nftables_search: @@ -187,7 +352,53 @@ class VyOSUnitTestSHIM: break self.assertTrue(not matched if inverse else matched, msg=search) - def verify_nftables_chain_exists(self, table, chain, inverse=False): + def verify_nftables_chain_exists(self, table: str, chain: str, inverse: bool=False) -> None: + """ + Assert existence or non-existence of an nftables chain. + + Calls `sudo nft list chain {table} {chain}` and verifies whether the + chain does or does not exist. + + Usage: + nftables output excerpt: + ```text + chain VYOS_INPUT_filter { + ct state established accept + } + ``` + + ##### Example 1: + Verify a chain exists + + Code usage: + ```python + self.verify_nftables_chain_exists( + table="ip vyos_filter", chain="VYOS_INPUT_filter" + ) + ``` + + ##### Example 2 (inverse matching): + Verify a deprecated chain is not present + + Code usage: + ```python + self.verify_nftables_chain_exists( + table="ip VYOS_INPUT_filter", chain="deprecated_chain", inverse=True + ) + ``` + + Parameters: + table: str + Table spec accepted by nft (e.g. "ip vyos_filter" or + "ip6 vyos_filter"). + chain: str + Chain name within the specified table. + inverse: bool + If True, assert the chain does not exist. If False, assert it exists. + + Raises: + AssertionError: If expectations are not met. + """ try: cmd(f'sudo nft list chain {table} {chain}') if inverse: @@ -208,6 +419,31 @@ class VyOSUnitTestSHIM: break self.assertTrue(not matched if inverse else matched, msg=search) + @staticmethod + def wait_for_result(runnable, check, pause=1, timeout=10): + """ + Run `runnable` each `pause` seconds till `timeout` seconds is over. + Each time compare return value with `check` if it is not callable, if + it is callable check if `check(result)` is True. + + @returns tuple (check_result, last_return_value). check_result is True + if (last_return_value == check) for non-callable check or if (check(last_return_value)) + is true. + """ + tries = ceil(timeout / pause) + result = None + for i in range(tries): + result = runnable() + if callable(check): + if check(result): + return True, result + elif result == check: + return True, result + + sleep(pause) + + return False, result + # standard construction; typing suggestion: https://stackoverflow.com/a/70292317 def ignore_warning(warning: Type[Warning]): import warnings diff --git a/smoketest/scripts/cli/test_backslash_escape.py b/smoketest/scripts/cli/test_backslash_escape.py index e94e9ab0a..0fe55f0f4 100755 --- a/smoketest/scripts/cli/test_backslash_escape.py +++ b/smoketest/scripts/cli/test_backslash_escape.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -65,4 +65,4 @@ class TestBackslashEscape(VyOSUnitTestSHIM.TestCase): self.cli_commit() if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_cgnat.py b/smoketest/scripts/cli/test_cgnat.py index 02dad3de5..541fc9fc5 100755 --- a/smoketest/scripts/cli/test_cgnat.py +++ b/smoketest/scripts/cli/test_cgnat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -20,11 +20,9 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError - base_path = ['nat', 'cgnat'] nftables_cgnat_config = '/run/nftables-cgnat.nft' - class TestCGNAT(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): @@ -39,6 +37,9 @@ class TestCGNAT(VyOSUnitTestSHIM.TestCase): self.cli_commit() self.assertFalse(os.path.exists(nftables_cgnat_config)) + # always forward to base class + super().tearDown() + def test_cgnat(self): internal_name = 'vyos-int-01' external_name = 'vyos-ext-01' @@ -135,4 +136,4 @@ class TestCGNAT(VyOSUnitTestSHIM.TestCase): if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_config_dependency.py b/smoketest/scripts/cli/test_config_dependency.py index 99e807ac5..fe5a371dd 100755 --- a/smoketest/scripts/cli/test_config_dependency.py +++ b/smoketest/scripts/cli/test_config_dependency.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -14,7 +14,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see <http://www.gnu.org/licenses/>. - import unittest from time import sleep @@ -24,7 +23,6 @@ from vyos.configsession import ConfigSessionError from base_vyostest_shim import VyOSUnitTestSHIM - class TestConfigDep(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): @@ -127,4 +125,4 @@ class TestConfigDep(VyOSUnitTestSHIM.TestCase): self.cli_commit() if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_config_save.py b/smoketest/scripts/cli/test_config_save.py new file mode 100755 index 000000000..aaf79cca3 --- /dev/null +++ b/smoketest/scripts/cli/test_config_save.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> +# +# 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 vyos.utils.process import cmd +from vyos.utils.config import read_saved_value +from vyos.defaults import directories + +from base_vyostest_shim import VyOSUnitTestSHIM + +class TestConfigDep(VyOSUnitTestSHIM.TestCase): + def test_disk_resident(self): + config_file = os.path.join(directories['config'], 'config.boot') + + evict_cmd = f'vmtouch -e {config_file}' + page_count_cmd = f'fincore -o PAGES -n {config_file}' + + test_value = 'test_disk_resident' + test_path = ['interfaces', 'ethernet', 'eth3', 'description'] + + self.cli_set(test_path, value=test_value) + self.cli_commit() + self.cli_save(config_file) + + cmd(evict_cmd) + # pages may be paged back into memory by the time the above + # completes (man vmtouch); either way, we read what is resident on + # disk. The following is just for curiosity: + pages = cmd(page_count_cmd) + + saved_value = read_saved_value(test_path) + + if self.debug: + print(f'vm pages on read config: {int(pages)}') + + self.assertEqual(test_value, saved_value) + + # clean up remaining + self.cli_delete(test_path) + self.cli_commit() + self.cli_save(config_file) + + def test_disk_resident_atomic(self): + config_file = os.path.join(directories['config'], 'config.boot') + + # save config will only call write_file_atomic if euid == 0: + # below is the command as invoked by CLI 'save' + save_cmd = ( + 'sudo sg vyattacfg "umask 0002; /usr/libexec/vyos/vyos-save-config.py"' + ) + + evict_cmd = f'vmtouch -e {config_file}' + page_count_cmd = f'fincore -o PAGES -n {config_file}' + + test_value = 'test_disk_resident' + test_path = ['interfaces', 'ethernet', 'eth3', 'description'] + + self.cli_set(test_path, value=test_value) + self.cli_commit() + cmd(save_cmd) + + cmd(evict_cmd) + # pages may be paged back into memory by the time the above + # completes (man vmtouch); either way, we read what is resident on + # disk. The following is just for curiosity: + pages = cmd(page_count_cmd) + + saved_value = read_saved_value(test_path) + + if self.debug: + print(f'vm pages on read config: {int(pages)}') + + # check that we have at the least sync'd config; + # checking actual atomicity is a different matter ... + self.assertEqual(test_value, saved_value) + + # clean up remaining + self.cli_delete(test_path) + self.cli_commit() + cmd(save_cmd) + + +if __name__ == '__main__': + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_configd_init.py b/smoketest/scripts/cli/test_configd_init.py index 245c03824..cf9d40b94 100755 --- a/smoketest/scripts/cli/test_configd_init.py +++ b/smoketest/scripts/cli/test_configd_init.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,23 +17,31 @@ import unittest from time import sleep +from base_vyostest_shim import VyOSUnitTestSHIM + from vyos.utils.process import is_systemd_service_running from vyos.utils.process import cmd +service_name = 'vyos-configd.service' + class TestConfigdInit(unittest.TestCase): def setUp(self): - self.running_state = is_systemd_service_running('vyos-configd.service') + self.running_state = is_systemd_service_running(service_name) + # always forward to base class + super().setUp() + + def tearDown(self): + if not self.running_state: + cmd(f'sudo systemctl stop {service_name}') + # always forward to base class + super().tearDown() def test_configd_init(self): if not self.running_state: - cmd('sudo systemctl start vyos-configd.service') + cmd(f'sudo systemctl start {service_name}') # allow time for init to succeed/fail sleep(2) - self.assertTrue(is_systemd_service_running('vyos-configd.service')) - - def tearDown(self): - if not self.running_state: - cmd('sudo systemctl stop vyos-configd.service') + self.assertTrue(is_systemd_service_running(service_name)) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_container.py b/smoketest/scripts/cli/test_container.py index 36622cad1..237e6238d 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-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -23,6 +23,7 @@ from base_vyostest_shim import VyOSUnitTestSHIM from ipaddress import ip_interface from vyos.configsession import ConfigSessionError +from vyos.utils.network import get_interface_vrf from vyos.utils.process import cmd from vyos.utils.process import process_named_running @@ -51,6 +52,7 @@ class TestContainer(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']) @classmethod def tearDownClass(cls): @@ -68,19 +70,34 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): # Ensure systemd units are removed units = glob.glob('/run/systemd/system/vyos-container-*') self.assertEqual(units, []) + # always forward to base class + super().tearDown() def test_basic(self): 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', 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', + ] + ) + self.cli_set(base_path + ['name', cont_name, 'log-driver', 'journald']) # commit changes self.cli_commit() @@ -95,17 +112,51 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): tmp = cmd(f'sudo podman exec -it {cont_name} sysctl kernel.msgmax') self.assertEqual(tmp, 'kernel.msgmax = 4096') + l = cmd_to_json(f'sudo podman container inspect {cont_name}') + self.assertEqual(l['HostConfig']['LogConfig']['Type'], 'journald') + self.assertEqual(l['Config']['Healthcheck']['Test'], ['NONE']) + + def test_healthcheck(self): + cont_name = 'health-test' + + self.cli_set(base_path + ['name', cont_name, 'allow-host-networks']) + self.cli_set(base_path + ['name', cont_name, 'image', busybox_image]) + + self.cli_set(base_path + ['name', cont_name, 'health-check', 'command', 'true']) + self.cli_set(base_path + ['name', cont_name, 'health-check', 'interval', '10']) + self.cli_set(base_path + ['name', cont_name, 'health-check', 'timeout', '1']) + self.cli_set(base_path + ['name', cont_name, 'health-check', 'retry', '2']) + self.cli_commit() + + l = cmd_to_json(f'sudo podman container inspect {cont_name}') + self.assertEqual(l['HostConfig']['LogConfig']['Type'], 'journald') + self.assertEqual(l['Config']['Healthcheck']['Test'], ['CMD-SHELL', 'true']) + self.assertEqual(l['Config']['Healthcheck']['Interval'], 10000000000) + self.assertEqual(l['Config']['Healthcheck']['Timeout'], 1000000000) + self.assertEqual(l['Config']['Healthcheck']['Retries'], 2) + def test_name_server(self): cont_name = 'dns-test' net_name = 'net-test' - name_server = '192.168.0.1' + name_servers = ['192.168.0.1', '192.168.0.2'] 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)]) + for name_server in name_servers: + 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): @@ -115,7 +166,7 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): self.cli_commit() n = cmd_to_json(f'sudo podman inspect {cont_name}') - self.assertEqual(n['HostConfig']['Dns'][0], name_server) + self.assertEqual(n['HostConfig']['Dns'], name_servers) tmp = cmd(f'sudo podman exec -it {cont_name} cat /etc/resolv.conf') self.assertIn(name_server, tmp) @@ -136,6 +187,83 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): # Check for running process self.assertEqual(process_named_running(PROCESS_NAME), pid) + def test_network_types(self): + self.cli_set(['interfaces', 'ethernet', 'eth0', 'vif', '100']) + self.cli_set(['interfaces', 'ethernet', 'eth0', 'vif', '101']) + + # MACVLAN Networks + self.cli_set(base_path + ['network', 'macvlan1', 'prefix', '10.0.0.0/24']) + self.cli_set(base_path + ['network', 'macvlan1', 'type', 'macvlan', 'parent', 'eth0']) + self.cli_set(base_path + ['network', 'macvlan1', 'type', 'macvlan', 'mode', 'bridge']) + self.cli_set(base_path + ['network', 'macvlan2', 'prefix', '10.0.100.0/24']) + self.cli_set(base_path + ['network', 'macvlan2', 'gateway', '10.0.100.5']) + self.cli_set(base_path + ['network', 'macvlan2', 'type', 'macvlan', 'parent', 'eth0.100']) + self.cli_set(base_path + ['network', 'macvlan2', 'type', 'macvlan', 'mode', 'private']) + self.cli_set(base_path + ['network', 'macvlan3', 'prefix', '2001::/64']) + self.cli_set(base_path + ['network', 'macvlan3', 'type', 'macvlan', 'parent', 'eth0.101']) + self.cli_set(base_path + ['network', 'macvlan3', 'type', 'macvlan', 'mode', 'vepa']) + + # Bridge Network + self.cli_set(base_path + ['network', 'bridge1', 'prefix', '10.0.1.0/24']) + self.cli_set(base_path + ['network', 'bridge1', 'type', 'bridge']) + + # Bridge Network before T7186; default network type is bridge + self.cli_set(base_path + ['network', 'bridge2', 'prefix', '10.0.2.0/24']) + + self.cli_commit() + + n = cmd_to_json(f'sudo podman network inspect macvlan1') + self.assertEqual(n['driver'], 'macvlan') + self.assertEqual(n['network_interface'], 'eth0') + self.assertEqual(n['options']['mode'], 'bridge') + self.assertEqual(n['subnets'][0]['subnet'], '10.0.0.0/24') + self.assertEqual(n['subnets'][0]['gateway'], '10.0.0.1') + + n = cmd_to_json(f'sudo podman network inspect macvlan2') + self.assertEqual(n['driver'], 'macvlan') + self.assertEqual(n['network_interface'], 'eth0.100') + self.assertEqual(n['options']['mode'], 'private') + self.assertEqual(n['subnets'][0]['subnet'], '10.0.100.0/24') + self.assertEqual(n['subnets'][0]['gateway'], '10.0.100.5') + + n = cmd_to_json(f'sudo podman network inspect macvlan3') + self.assertEqual(n['driver'], 'macvlan') + self.assertEqual(n['network_interface'], 'eth0.101') + self.assertEqual(n['options']['mode'], 'vepa') + self.assertEqual(n['subnets'][0]['subnet'], '2001::/64') + self.assertEqual(n['subnets'][0]['gateway'], '2001::1') + + n = cmd_to_json(f'sudo podman network inspect bridge1') + self.assertEqual(n['driver'], 'bridge') + self.assertEqual(n['network_interface'], 'pod-bridge1') + self.assertEqual(n['subnets'][0]['subnet'], '10.0.1.0/24') + self.assertEqual(n['subnets'][0]['gateway'], '10.0.1.1') + + n = cmd_to_json(f'sudo podman network inspect bridge2') + self.assertEqual(n['driver'], 'bridge') + self.assertEqual(n['network_interface'], 'pod-bridge2') + self.assertEqual(n['subnets'][0]['subnet'], '10.0.2.0/24') + self.assertEqual(n['subnets'][0]['gateway'], '10.0.2.1') + + def test_user_defined_mac(self): + # Bridge Network + self.cli_set(base_path + ['network', 'bridge1', 'prefix', '10.0.1.0/24']) + self.cli_set(base_path + ['network', 'bridge1', 'type', 'bridge']) + + self.cli_set(base_path + ['name', "test1", 'image', busybox_image]) + self.cli_set(base_path + ['name', "test1", 'network', 'bridge1', 'address', '10.0.1.11']) + self.cli_set(base_path + ['name', "test1", 'network', 'bridge1', 'mac', '02:00:00:00:00:01']) + + self.cli_set(base_path + ['name', "test2", 'image', busybox_image]) + self.cli_set(base_path + ['name', "test2", 'network', 'bridge1', 'address', '10.0.1.12']) + self.cli_set(base_path + ['name', "test2", 'network', 'bridge1', 'mac', '02:00:00:00:00:02']) + self.cli_commit() + + n = cmd_to_json(f'sudo podman container inspect test1') + self.assertEqual(n['NetworkSettings']['Networks']['bridge1']['MacAddress'], '02:00:00:00:00:01') + n = cmd_to_json(f'sudo podman container inspect test2') + self.assertEqual(n['NetworkSettings']['Networks']['bridge1']['MacAddress'], '02:00:00:00:00:02') + def test_ipv4_network(self): prefix = '192.0.2.0/24' base_name = 'ipv4' @@ -146,7 +274,17 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): for ii in range(1, 6): name = f'{base_name}-{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)]) + 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): @@ -159,12 +297,18 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): n = cmd_to_json(f'sudo podman network inspect {net_name}') self.assertEqual(n['subnets'][0]['subnet'], prefix) - # skipt first container, it was never created + # skip first container, it was never created 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' @@ -176,7 +320,17 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): for ii in range(1, 6): name = f'{base_name}-{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)]) + 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): @@ -189,12 +343,18 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): n = cmd_to_json(f'sudo podman network inspect {net_name}') self.assertEqual(n['subnets'][0]['subnet'], prefix) - # skipt first container, it was never created + # skip first container, it was never created 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' @@ -208,8 +368,28 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): for ii in range(1, 6): name = f'{base_name}-{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)]) + 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): @@ -223,14 +403,26 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): self.assertEqual(n['subnets'][0]['subnet'], prefix4) self.assertEqual(n['subnets'][1]['subnet'], prefix6) - # skipt first container, it was never created + # skip first container, it was never created 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' @@ -242,7 +434,17 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): 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_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}') @@ -258,7 +460,17 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): 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_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}') @@ -298,11 +510,38 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): 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)) + def test_network_vrf(self): + cont_name = 'vrf-test50' + net_name = 'vrf-test50' + vrf_name = 'red-15' + + # create temporary VRF for testing + self.cli_set(['vrf', 'name', vrf_name, 'table', '100']) + + self.cli_set(base_path + ['name', cont_name, 'image', busybox_image]) + self.cli_set(base_path + ['name', cont_name, 'network', net_name]) + self.cli_set(base_path + ['network', net_name, 'prefix', '192.168.0.0/24']) + self.cli_set(base_path + ['network', net_name, 'vrf', vrf_name]) + + self.cli_commit() + + tmp = get_interface_vrf(f'pod-{net_name}') + self.assertEqual(tmp, vrf_name) + + # Restart container and validate VRF assignment + self.op_mode(['restart', 'container', cont_name]) + tmp = get_interface_vrf(f'pod-{net_name}') + self.assertEqual(tmp, vrf_name) + + self.cli_delete(['vrf', 'name', vrf_name]) + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index 2829edbfb..224c29a5e 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-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -71,6 +71,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ] self.verify_nftables(nftables_search, 'ip vyos_filter', inverse=True) + # always forward to base class + super().tearDown() def wait_for_domain_resolver(self, table, set_name, element, max_wait=10): # Resolver no longer blocks commit, need to wait for daemon to populate set @@ -290,6 +292,33 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip vyos_filter') + def test_ipv4_time_weekdays(self): + name = 'v4-time-weekdays-test' + rule_base = ['firewall', 'ipv4', 'name', name, 'rule', '10'] + + self.cli_set(rule_base + ['action', 'accept']) + self.cli_set(rule_base + ['time', 'weekdays', 'mon,friday']) + + self.cli_commit() + + nftables_search = [ + [f'chain NAME_{name}'], + ['meta day { "Monday", "Friday" }'], + ] + + self.verify_nftables(nftables_search, 'ip vyos_filter') + + self.cli_set(rule_base + ['time', 'weekdays', 'Monday, Sat']) + + self.cli_commit() + + nftables_search = [ + [f'chain NAME_{name}'], + ['meta day { "Monday", "Saturday" }'], + ] + + self.verify_nftables(nftables_search, 'ip vyos_filter') + def test_ipv4_advanced(self): name = 'smoketest-adv' name2 = 'smoketest-adv2' @@ -401,9 +430,9 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_commit() nftables_search = [ - [f'daddr & 0.0.255.255 == 0.0.1.2'], - [f'saddr & 0.0.255.255 != 0.0.3.4'], - [f'saddr & 0.0.255.255 == @A_mask_group'] + ['daddr & 0.0.255.255 == 0.0.1.2'], + ['saddr & 0.0.255.255 != 0.0.3.4'], + ['saddr & 0.0.255.255 == @A_mask_group'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') @@ -411,9 +440,11 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): def test_ipv4_dynamic_groups(self): group01 = 'knock01' group02 = 'allowed' + group03 = 'restricted' self.cli_set(['firewall', 'group', 'dynamic-group', 'address-group', group01]) self.cli_set(['firewall', 'group', 'dynamic-group', 'address-group', group02]) + self.cli_set(['firewall', 'group', 'dynamic-group', 'address-group', group03]) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'action', 'drop']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'protocol', 'tcp']) @@ -433,18 +464,26 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '30', 'destination', 'port', '22']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '30', 'source', 'group', 'dynamic-address-group', group02]) + self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '40', 'action', 'drop']) + self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '40', 'protocol', 'tcp']) + self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '40', 'destination', 'port', '6667']) + self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '40', 'add-address-to-group', 'destination-address', 'address-group', group03]) + self.cli_commit() nftables_search = [ [f'DA_{group01}'], [f'DA_{group02}'], + [f'DA_{group03}'], ['type ipv4_addr'], ['flags dynamic,timeout'], ['chain VYOS_INPUT_filter {'], ['type filter hook input priority filter', 'policy accept'], ['tcp dport 5151', f'update @DA_{group01}', '{ ip saddr timeout 30s }', 'drop'], ['tcp dport 7272', f'ip saddr @DA_{group01}', f'update @DA_{group02}', '{ ip saddr timeout 5m }', 'drop'], - ['tcp dport 22', f'ip saddr @DA_{group02}', 'accept'] + ['tcp dport 22', f'ip saddr @DA_{group02}', 'accept'], + ['chain VYOS_FORWARD_filter {'], + ['tcp dport 6667', f'update @DA_{group03}', '{ ip daddr }', 'drop'], ] self.verify_nftables(nftables_search, 'ip vyos_filter') @@ -528,6 +567,33 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip6 vyos_filter') + def test_ipv6_time_weekdays(self): + name = 'v6-time-weekdays-test' + rule_base = ['firewall', 'ipv6', 'name', name, 'rule', '10'] + + self.cli_set(rule_base + ['action', 'accept']) + self.cli_set(rule_base + ['time', 'weekdays', 'mon,friday']) + + self.cli_commit() + + nftables_search = [ + [f'chain NAME6_{name}'], + ['meta day { "Monday", "Friday" }'], + ] + + self.verify_nftables(nftables_search, 'ip6 vyos_filter') + + self.cli_set(rule_base + ['time', 'weekdays', 'Monday, Sat']) + + self.cli_commit() + + nftables_search = [ + [f'chain NAME6_{name}'], + ['meta day { "Monday", "Saturday" }'], + ] + + self.verify_nftables(nftables_search, 'ip6 vyos_filter') + def test_ipv6_advanced(self): name = 'v6-smoke-adv' @@ -603,9 +669,11 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): def test_ipv6_dynamic_groups(self): group01 = 'knock01' group02 = 'allowed' + group03 = 'restricted' self.cli_set(['firewall', 'group', 'dynamic-group', 'ipv6-address-group', group01]) self.cli_set(['firewall', 'group', 'dynamic-group', 'ipv6-address-group', group02]) + self.cli_set(['firewall', 'group', 'dynamic-group', 'ipv6-address-group', group03]) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'action', 'drop']) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'protocol', 'tcp']) @@ -625,23 +693,35 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '30', 'destination', 'port', '22']) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '30', 'source', 'group', 'dynamic-address-group', group02]) + self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '40', 'action', 'drop']) + self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '40', 'protocol', 'tcp']) + self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '40', 'destination', 'port', '6667']) + self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '40', 'add-address-to-group', 'destination-address', 'address-group', group03]) + self.cli_commit() nftables_search = [ [f'DA6_{group01}'], [f'DA6_{group02}'], + [f'DA6_{group03}'], ['type ipv6_addr'], ['flags dynamic,timeout'], ['chain VYOS_IPV6_INPUT_filter {'], ['type filter hook input priority filter', 'policy accept'], ['tcp dport 5151', f'update @DA6_{group01}', '{ ip6 saddr timeout 30s }', 'drop'], ['tcp dport 7272', f'ip6 saddr @DA6_{group01}', f'update @DA6_{group02}', '{ ip6 saddr timeout 5m }', 'drop'], - ['tcp dport 22', f'ip6 saddr @DA6_{group02}', 'accept'] + ['tcp dport 22', f'ip6 saddr @DA6_{group02}', 'accept'], + ['chain VYOS_IPV6_FORWARD_filter {'], + ['tcp dport 6667', f'update @DA6_{group03}', '{ ip6 daddr }', 'drop'], ] 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']) @@ -651,6 +731,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'] @@ -721,7 +804,13 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'group', 'ipv6-address-group', 'AGV6', 'address', '2001:db1::1']) self.cli_set(['firewall', 'global-options', 'state-policy', 'established', 'action', 'accept']) self.cli_set(['firewall', 'global-options', 'apply-to-bridged-traffic', 'ipv4']) - self.cli_set(['firewall', 'global-options', 'apply-to-bridged-traffic', 'invalid-connections']) + self.cli_set(['firewall', 'global-options', 'apply-to-bridged-traffic', 'accept-invalid', 'ethernet-type', 'dhcp']) + self.cli_set(['firewall', 'global-options', 'apply-to-bridged-traffic', 'accept-invalid', 'ethernet-type', 'arp']) + self.cli_set(['firewall', 'global-options', 'apply-to-bridged-traffic', 'accept-invalid', 'ethernet-type', 'pppoe']) + self.cli_set(['firewall', 'global-options', 'apply-to-bridged-traffic', 'accept-invalid', 'ethernet-type', 'pppoe-discovery']) + self.cli_set(['firewall', 'global-options', 'apply-to-bridged-traffic', 'accept-invalid', 'ethernet-type', '802.1q']) + self.cli_set(['firewall', 'global-options', 'apply-to-bridged-traffic', 'accept-invalid', 'ethernet-type', '802.1ad']) + self.cli_set(['firewall', 'global-options', 'apply-to-bridged-traffic', 'accept-invalid', 'ethernet-type', 'wol']) self.cli_set(['firewall', 'bridge', 'name', name, 'default-action', 'accept']) self.cli_set(['firewall', 'bridge', 'name', name, 'default-log']) @@ -776,7 +865,11 @@ 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 8021q', 'accept'], + ['ct state invalid', 'ether type 8021ad', 'accept'], + ['ct state invalid', 'ether type 0x8863', 'accept'], ['ct state invalid', 'ether type 0x8864', 'accept'], + ['ct state invalid', 'ether type 0x0842', 'accept'], ['chain VYOS_PREROUTING_filter'], ['type filter hook prerouting priority filter; policy accept;'], ['ip6 daddr @A6_AGV6', 'notrack'], @@ -927,6 +1020,20 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'global-options', 'state-policy', 'related', 'action', 'accept']) self.cli_set(['firewall', 'global-options', 'state-policy', 'invalid', 'action', 'drop']) + # Test error on offload from local zone + self.cli_set(['firewall', 'flowtable', 'smoketest', 'interface', 'eth0']) + self.cli_set(['firewall', 'flowtable', 'smoketest', 'offload', 'software']) + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'action', 'offload']) + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'offload-target', 'smoketest']) + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'state', 'established']) + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'state', 'related']) + + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete(['firewall', 'flowtable', 'smoketest']) + self.cli_delete(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1']) + self.cli_commit() nftables_search = [ @@ -975,6 +1082,50 @@ 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_default_firewall(self): + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'default-action', 'drop']) + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-default', 'default-action', 'drop']) + self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'member', 'interface', 'eth0']) + self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'from', 'smoketest-eth1', 'firewall', 'name', 'smoketest']) + self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'from', 'smoketest-local', 'firewall', 'name', 'smoketest']) + self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'default-firewall', 'name', 'smoketest-default']) + self.cli_set(['firewall', 'zone', 'smoketest-eth1', 'member', 'interface', 'eth1']) + self.cli_set(['firewall', 'zone', 'smoketest-eth1', 'default-firewall', 'name', 'smoketest-default']) + self.cli_set(['firewall', 'zone', 'smoketest-eth2', 'member', 'interface', 'eth2']) + self.cli_set(['firewall', 'zone', 'smoketest-local', 'local-zone']) + self.cli_set(['firewall', 'zone', 'smoketest-local', 'from', 'smoketest-eth0', 'firewall', 'name', 'smoketest']) + self.cli_set(['firewall', 'zone', 'smoketest-local', 'default-firewall', 'name', 'smoketest-default']) + self.cli_commit() + + smoketest_eth0_search = [ + ['iifname "eth1"', 'jump NAME_smoketest'], + ['jump NAME_smoketest-default'] + ] + self.verify_nftables_chain_exists('ip vyos_filter', 'VZONE_smoketest-eth0') + self.verify_nftables_chain(smoketest_eth0_search, 'ip vyos_filter', 'VZONE_smoketest-eth0') + + smoketest_eth1_search = [ + ['jump NAME_smoketest-default'] + ] + self.verify_nftables_chain_exists('ip vyos_filter', 'VZONE_smoketest-eth1') + self.verify_nftables_chain(smoketest_eth1_search, 'ip vyos_filter', 'VZONE_smoketest-eth1') + + self.verify_nftables_chain_exists('ip vyos_filter', 'VZONE_smoketest-eth2') + + smoketest_local_in_search = [ + ['iifname "eth0"', 'jump NAME_smoketest'], + ['jump NAME_smoketest-default'], + ] + self.verify_nftables_chain_exists('ip vyos_filter', 'VZONE_smoketest-local_IN') + self.verify_nftables_chain(smoketest_local_in_search, 'ip vyos_filter', 'VZONE_smoketest-local_IN') + + smoketest_local_out_search = [ + ['oifname "eth0"', 'jump NAME_smoketest'], + ['oifname "eth1"', 'jump NAME_smoketest-default'] + ] + self.verify_nftables_chain_exists('ip vyos_filter', 'VZONE_smoketest-local_OUT') + self.verify_nftables_chain(smoketest_local_out_search, 'ip vyos_filter', 'VZONE_smoketest-local_OUT') + 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']) @@ -1004,9 +1155,9 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['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 "VRF-1"', 'counter packets', 'jump VZONE_ZONE1'], ['oifname "vtun66"', 'counter packets', 'jump VZONE_ZONE2'], - ['oifname "vti1"', 'counter packets', 'jump VZONE_ZONE2'], + ['oifname "VRF-2"', 'counter packets', 'jump VZONE_ZONE2'], ['chain VYOS_ZONE_LOCAL'], ['type filter hook input priority filter + 1'], ['counter packets', 'jump VZONE_LOCAL_IN'], @@ -1039,9 +1190,9 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['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 "VRF-1"', 'counter packets', 'jump VZONE_ZONE1'], ['oifname "vtun66"', 'counter packets', 'jump VZONE_ZONE2'], - ['oifname "vti1"', 'counter packets', 'jump VZONE_ZONE2'], + ['oifname "VRF-2"', 'counter packets', 'jump VZONE_ZONE2'], ['chain VYOS_ZONE_LOCAL'], ['type filter hook input priority filter + 1'], ['counter packets', 'jump VZONE_LOCAL_IN'], @@ -1052,7 +1203,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['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'], + ['oifname "VRF-2"', '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'], @@ -1067,6 +1218,43 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip vyos_filter') self.verify_nftables(nftables_search_v6, 'ip6 vyos_filter') + def test_zone_without_member(self): + self.cli_set(['firewall', 'zone', 'wan', 'default-action', 'drop']) + error_message = 'Zone "wan" has no interfaces and is not the local zone' + with self.assertRaisesRegex(ConfigSessionError, error_message): + self.cli_commit() + + self.cli_set(['firewall', 'zone', 'wan', 'member', 'interface', 'eth1']) + self.cli_commit() + + def test_wildcard_interfaces(self): + wc_interfaces = [ + 'eth0', + 'eth0.*', + 'eth1', + 'eth1.23.*', + 'eth2.5.25', + 'eth2.5.25.54', + 'eth3*', + 'eth4.*', + 'ipoe*', + 'peth3', + 'peth3.', + 'pod-one', + 'pppoe*', + 'pptp*', + 'l2tp*', + 'sstp*', + 'vpptun*', + ] + for iface in wc_interfaces: + self.cli_set( + ['firewall', 'zone', 'smoketest-wildcard', 'member', 'interface', iface] + ) + self.cli_commit() + + self.verify_nftables(wc_interfaces, 'ip vyos_filter') + def test_flow_offload(self): self.cli_set(['interfaces', 'ethernet', 'eth0', 'vif', '10']) self.cli_set(['firewall', 'flowtable', 'smoketest', 'interface', 'eth0.10']) @@ -1106,6 +1294,12 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables_chain([['accept']], 'ip vyos_conntrack', 'FW_CONNTRACK') self.verify_nftables_chain([['accept']], 'ip6 vyos_conntrack', 'FW_CONNTRACK') + # Test interface deletion + self.cli_delete(['interfaces', 'ethernet', 'eth0', 'vif', '10']) + + with self.assertRaises(ConfigSessionError): + self.cli_commit() + def test_zone_flow_offload(self): self.cli_set(['firewall', 'flowtable', 'smoketest', 'interface', 'eth0']) self.cli_set(['firewall', 'flowtable', 'smoketest', 'offload', 'hardware']) @@ -1288,7 +1482,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['R_group01'], ['type ipv4_addr'], ['flags interval'], - ['meta l4proto', 'daddr @R_group01', "ipv4-INP-filter-10"] + ['meta l4proto', 'daddr @R_group01', 'ipv4-INP-filter-10'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') @@ -1307,5 +1501,129 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): 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') + + + def test_disable_conntrack_per_chain(self): + # If conntrack is disabled in either the input or output chain, + # state cannot be matched in either the input or outchain + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'disable-conntrack']) + self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'action', 'accept']) + self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'state', 'established']) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_discard() + + # If conntrack is disabled in the forward chain, + # state cannot be matched in the forward chain + self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'disable-conntrack']) + self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'accept']) + self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'state', 'established']) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_discard() + + # Disable conntrack in all chains for both ipv4 and ipv6 + self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'disable-conntrack']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'disable-conntrack']) + self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'disable-conntrack']) + self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'disable-conntrack']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'disable-conntrack']) + self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'disable-conntrack']) + + self.cli_commit() + + nftables_search = [ + ['VYOS_DISABLE_CONNTRACK_INP_FWD'], + ['VYOS_DISABLE_CONNTRACK_OUT'], + ['fib daddr . iif type unicast notrack counter'], + ['fib daddr . iif type local notrack counter '] + ] + + self.verify_nftables(nftables_search, 'ip vyos_filter') + + nftables_search = [ + ['VYOS_DISABLE_CONNTRACK_INP_FWD_V6'], + ['VYOS_DISABLE_CONNTRACK_OUT_V6'], + ['fib daddr . iif type unicast notrack counter'], + ['fib daddr . iif type local notrack counter '] + ] + + self.verify_nftables(nftables_search, 'ip6 vyos_filter') + + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_high-availability_virtual-server.py b/smoketest/scripts/cli/test_high-availability_virtual-server.py index 2dbf4a5f2..7939f139e 100755 --- a/smoketest/scripts/cli/test_high-availability_virtual-server.py +++ b/smoketest/scripts/cli/test_high-availability_virtual-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -38,6 +38,8 @@ class TestHAVirtualServer(VyOSUnitTestSHIM.TestCase): # Process must be terminated after deleting the config self.assertFalse(process_named_running(PROCESS_NAME)) + # always forward to base class + super().tearDown() def test_01_ha_virtual_server(self): algo = 'least-connection' @@ -81,6 +83,13 @@ class TestHAVirtualServer(VyOSUnitTestSHIM.TestCase): self.assertIn(f'{proto.upper()}_CHECK', config) self.assertIn(f'connect_timeout {connection_timeout}', config) + # Verify persistence_timeout is not set when value is 0 + self.cli_set(vserver_base + [vs, 'persistence-timeout', '0']) + self.cli_commit() + + config = read_file(KEEPALIVED_CONF) + self.assertNotIn('persistence_timeout', config) + def test_02_ha_virtual_server_and_vrrp(self): algo = 'least-connection' delay = '15' @@ -146,4 +155,4 @@ class TestHAVirtualServer(VyOSUnitTestSHIM.TestCase): if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_high-availability_vrrp.py b/smoketest/scripts/cli/test_high-availability_vrrp.py index aa9fa432e..93ef9a09b 100755 --- a/smoketest/scripts/cli/test_high-availability_vrrp.py +++ b/smoketest/scripts/cli/test_high-availability_vrrp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -50,6 +50,8 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): # Process must be terminated after deleting the config self.assertFalse(process_named_running(PROCESS_NAME)) + # always forward to base class + super().tearDown() def test_01_default_values(self): for group in groups: @@ -135,6 +137,9 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): self.cli_set(global_param_base + ['garp', 'master-refresh-repeat', f'{garp_master_refresh_repeat}']) self.cli_set(global_param_base + ['version', vrrp_version]) + # SNMP + self.cli_set(base_path + ['vrrp', 'snmp', 'trap']) + # commit changes self.cli_commit() @@ -147,6 +152,7 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): self.assertIn(f'vrrp_garp_master_refresh {garp_master_refresh}', config) self.assertIn(f'vrrp_garp_master_refresh_repeat {garp_master_refresh_repeat}', config) self.assertIn(f'vrrp_version {vrrp_version}', config) + self.assertIn('enable_traps', config) for group in groups: vlan_id = group.lstrip('VLAN') @@ -172,6 +178,14 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): self.assertIn(f'garp_master_refresh {group_garp_master_refresh}', config) self.assertIn(f'garp_master_repeat {group_garp_master_repeat}', config) + # Remove SNMP traps + self.cli_delete(base_path + ['vrrp', 'snmp', 'trap']) + + # commit changes + self.cli_commit() + config = getConfig(f'global_defs') + self.assertNotIn('enable_traps', config) + def test_03_sync_group(self): sync_group = 'VyOS' @@ -265,6 +279,7 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): def test_check_health_script(self): sync_group = 'VyOS' + timeout = '100' for group in groups: vlan_id = group.lstrip('VLAN') @@ -315,6 +330,16 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): config = getConfig(f'vrrp_sync_group {sync_group}') self.assertIn(f'track_script', config) + self.cli_set( + base_path + + ['vrrp', 'sync-group', sync_group, 'health-check', 'timeout', timeout] + ) + # commit changes + self.cli_commit() + + config = getConfig(f'vrrp_script healthcheck_sg_{sync_group}') + self.assertIn(f'timeout {timeout}', config) + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py index f99fd0363..a4afae5da 100755 --- a/smoketest/scripts/cli/test_interfaces_bonding.py +++ b/smoketest/scripts/cli/test_interfaces_bonding.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,6 +18,7 @@ import os import unittest from base_interfaces_test import BasicInterfaceTest +from base_vyostest_shim import VyOSUnitTestSHIM from vyos.ifconfig import Section from vyos.ifconfig.interface import Interface @@ -29,7 +30,6 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): cls._base_path = ['interfaces', 'bonding'] - cls._mirror_interfaces = ['dum21354'] cls._members = [] # we need to filter out VLAN interfaces identified by a dot (.) @@ -62,6 +62,53 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase): slaves = read_file(f'/sys/class/net/{interface}/bonding/slaves').split() self.assertListEqual(slaves, self._members) + def test_bonding_keep_mac(self): + # T7571: A bond interface should always run from the physical interfaces + # MAC address and not a synthetic one. + base_mac = Interface(self._members[0]).get_mac() + + # configure member interfaces + for interface in self._interfaces: + for option in self._options.get(interface, []): + self.cli_set(self._base_path + [interface] + option.split()) + + self.cli_commit() + + # Verify bond interface MAC address matches the address of it's first member + for interface in self._interfaces: + mac = Interface(interface).get_mac() + self.assertEqual(mac, base_mac) + + def test_bonding_physical_macs(self): + macs = {} + # configure member interfaces + for interface in self._interfaces: + for member in self._members: + macs[member] = get_interface_config(member)['address'] + + for option in self._options.get(interface, []): + self.cli_set(self._base_path + [interface] + option.split()) + + self.cli_commit() + + # mac must match the MAC of the first interface + for interface in self._interfaces: + bond_mac = get_interface_config(interface)['address'] + self.assertEqual(bond_mac, macs[self._members[0]]) + + # remove all member interfaces from the bond + for interface in self._interfaces: + self.cli_delete(self._base_path + [interface, 'member']) + + self.cli_commit() + + # members must re-gain their old MAC address + for interface in self._interfaces: + for member in self._members: + tmp = Interface(member) + self.assertEqual(tmp.get_mac(), macs[member]) + self.assertEqual(tmp.get_admin_state(), 'up') + def test_bonding_remove_member(self): # T2515: when removing a bond member the previously enslaved/member # interface must be in its former admin-up/down state. Here we ensure @@ -293,7 +340,7 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase): id = '5' for interface in self._interfaces: - frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') + frrconfig = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(f' evpn mh es-id {id}', frrconfig) self.assertIn(f' evpn mh es-df-pref {id}', frrconfig) @@ -310,11 +357,35 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase): id = '5' for interface in self._interfaces: - frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') + frrconfig = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(f' evpn mh es-sys-mac 00:12:34:56:78:0{id}', frrconfig) self.assertIn(f' evpn mh uplink', frrconfig) id = int(id) + 1 + def test_bonding_member_mtu(self): + # This Smoketest only works on our CI platform where we force the NIC + # to virtio and an MTU of only 1500 bytes max + if not os.path.exists('/tmp/vyos.smoketests.hint'): + self.skipTest('Not running under VyOS CI/CD QEMU environment!') + + for interface in self._interfaces: + for option in self._options.get(interface, []): + self.cli_set(self._base_path + [interface] + option.split()) + + self.cli_set(self._base_path + [interface, 'mtu', '10000']) + + # check validate() - MTU of bond higher then virtio max MTU + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + for interface in self._interfaces: + for option in self._options.get(interface, []): + self.cli_set(self._base_path + [interface] + option.split()) + + self.cli_delete(self._base_path + [interface, 'mtu']) + + self.cli_commit() + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py index 4041b3ef3..a41ad1482 100755 --- a/smoketest/scripts/cli/test_interfaces_bridge.py +++ b/smoketest/scripts/cli/test_interfaces_bridge.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 os import json import unittest - -from base_interfaces_test import BasicInterfaceTest from copy import deepcopy from glob import glob +from base_interfaces_test import BasicInterfaceTest +from base_vyostest_shim import VyOSUnitTestSHIM + from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section from vyos.template import ip_from_cidr @@ -34,7 +35,6 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): cls._base_path = ['interfaces', 'bridge'] - cls._mirror_interfaces = ['dum21354'] cls._members = [] # we need to filter out VLAN interfaces identified by a dot (.) @@ -56,7 +56,7 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): def tearDown(self): for intf in self._interfaces: self.cli_delete(self._base_path + [intf]) - + # always forward to base class super().tearDown() def test_isolated_interfaces(self): @@ -508,6 +508,31 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): self.cli_delete(['interfaces', 'vxlan', vxlan_if]) self.cli_delete(['interfaces', 'ethernet', 'eth0', 'address', eth0_addr]) + def test_bridge_root_bpdu_guard(self): + # Test if both bpdu_guard and root_guard configured + self.cli_set(['interfaces', 'bridge', 'br0', 'stp']) + self.cli_set(['interfaces', 'bridge', 'br0', 'member', 'interface', 'eth0', 'bpdu-guard']) + self.cli_set(['interfaces', 'bridge', 'br0', 'member', 'interface', 'eth0', 'root-guard']) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_discard() + + # Test if bpdu_guard configured + self.cli_set(['interfaces', 'bridge', 'br0', 'stp']) + self.cli_set(['interfaces', 'bridge', 'br0', 'member', 'interface', 'eth0', 'bpdu-guard']) + self.cli_commit() + + tmp = read_file(f'/sys/class/net/eth0/brport/bpdu_guard') + self.assertEqual(tmp, '1') + + # Test if root_guard configured + self.cli_delete(['interfaces', 'bridge', 'br0']) + self.cli_set(['interfaces', 'bridge', 'br0', 'stp']) + self.cli_set(['interfaces', 'bridge', 'br0', 'member', 'interface', 'eth0', 'root-guard']) + self.cli_commit() + + tmp = read_file(f'/sys/class/net/eth0/brport/root_block') + self.assertEqual(tmp, '1') if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_interfaces_dummy.py b/smoketest/scripts/cli/test_interfaces_dummy.py index d96ec2c5d..c011a411b 100755 --- a/smoketest/scripts/cli/test_interfaces_dummy.py +++ b/smoketest/scripts/cli/test_interfaces_dummy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2021 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,16 @@ import unittest from base_interfaces_test import BasicInterfaceTest +from base_vyostest_shim import VyOSUnitTestSHIM class DummyInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): cls._base_path = ['interfaces', 'dummy'] cls._interfaces = ['dum435', 'dum8677', 'dum0931', 'dum089'] + cls._mirror_interfaces = ['eth0'] # call base-classes classmethod super(DummyInterfaceTest, cls).setUpClass() if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py index 2b421e942..06253cf9e 100755 --- a/smoketest/scripts/cli/test_interfaces_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_ethernet.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -20,24 +20,29 @@ import unittest from glob import glob from json import loads -from netifaces import AF_INET -from netifaces import AF_INET6 -from netifaces import ifaddresses +from socket import AF_INET +from socket import AF_INET6 +from netifaces import ifaddresses # pylint: disable = no-name-in-module from base_interfaces_test import BasicInterfaceTest +from base_vyostest_shim import VyOSUnitTestSHIM + from vyos.configsession import ConfigSessionError +from vyos.ethtool import Ethtool +from vyos.netlink import coalesce +from vyos.frrender import mgmt_daemon from vyos.ifconfig import Section 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 process_named_running from vyos.utils.process import popen class EthernetInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): cls._base_path = ['interfaces', 'ethernet'] - cls._mirror_interfaces = ['dum21354'] # We only test on physical interfaces and not VLAN (sub-)interfaces if 'TEST_ETH' in os.environ: @@ -85,6 +90,9 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): ] self.assertListEqual(tmp, []) + # check process health and continuity + self.assertEqual(self.mgmt_daemon_pid, process_named_running(mgmt_daemon)) + 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 @@ -147,16 +155,18 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): self.assertEqual(int(tmp), 0) def test_non_existing_interface(self): - unknonw_interface = self._base_path + ['eth667'] - self.cli_set(unknonw_interface) + unknonw_interface = 'eth667' + self.cli_set(self._base_path + [unknonw_interface]) # check validate() - interface does not exist - with self.assertRaises(ConfigSessionError): + with self.assertRaises(ConfigSessionError) as cm: self.cli_commit() + self.assertIn(f'Interface "{unknonw_interface}" does not exist!', + str(cm.exception)) # we need to remove this wrong interface from the configuration # manually, else tearDown() will have problem in commit() - self.cli_delete(unknonw_interface) + self.cli_delete(self._base_path + [unknonw_interface]) def test_speed_duplex_verify(self): for interface in self._interfaces: @@ -196,6 +206,85 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): self.assertEqual(max_rx, rx) self.assertEqual(max_tx, tx) + def test_ethtool_coalesce(self): + """ + Verify that coalesce configuration is correctly applied to the interface using netlink + """ + + for interface in self._interfaces: + base_path = self._base_path + [interface, 'interrupt-coalescing'] + ethtool = Ethtool(interface) + is_virtio = ethtool.get_driver_name() == 'virtio_net' + + with self.subTest(interface=interface): + # Verify coalesce support on the NIC before check + supported = ethtool.check_coalesce() + + # If ethtool reports completely unsupported feature, then the CLI commit + # should correctly raise a ConfigSessionError during commit + if not supported: + self.cli_set(base_path + ['rx-usecs', '32']) + self.cli_set(base_path + ['tx-usecs', '32']) + + msg = 'Driver does not fully support coalesce configuration' + with self.assertRaisesRegex(ConfigSessionError, msg): + self.cli_commit() + continue + + # To find out the supported features + supported_rx_usecs = ethtool.check_coalesce('rx_usecs') + supported_tx_usecs = ethtool.check_coalesce('tx_usecs') + supported_adaptive_rx = ethtool.check_coalesce('adaptive_rx') and not is_virtio + supported_adaptive_tx = ethtool.check_coalesce('adaptive_tx') and not is_virtio + + # Disabled adaptive modes and set custom values + if supported_rx_usecs: + self.cli_set(base_path + ['rx-usecs', '64']) + if supported_tx_usecs: + self.cli_set(base_path + ['tx-usecs', '64']) + + # Force adaptive to be disabled if it is already enabled + params = coalesce.get_coalesce(interface) + if supported_rx_usecs and params['adaptive_rx']: + cmd(f'sudo ethtool --coalesce {interface} adaptive-rx off') + if supported_tx_usecs and params['adaptive_tx']: + cmd(f'sudo ethtool --coalesce {interface} adaptive-tx off') + + # Commit CLI configuration to apply coalescing + self.cli_commit() + + # Query coalesce parameters after applying + params = coalesce.get_coalesce(interface) + + # Assertions: all should reflect configured values + if supported_rx_usecs: + # `virtio-net` doesn't correctly work with this parameter + self.assertEqual(params['rx_usecs'], 0 if is_virtio else 64) + if supported_tx_usecs: + # `virtio-net` doesn't correctly work with this parameter + self.assertEqual(params['tx_usecs'], 0 if is_virtio else 64) + + # Not all parameters are adjustable for some of NIC (`virtio-net`) + if supported_adaptive_rx: + # Now test enabling RX adaptive coalescing modes + self.cli_delete(base_path + ['rx-usecs']) + self.cli_set(base_path + ['adaptive-rx']) + + if supported_adaptive_tx: + # Now test enabling TX adaptive coalescing modes + self.cli_delete(base_path + ['tx-usecs']) + self.cli_set(base_path + ['adaptive-tx']) + + self.cli_commit() + + # Verify that adaptive modes turned on correctly + params = coalesce.get_coalesce(interface) + if supported_adaptive_rx: + self.assertTrue(params['adaptive_rx']) + + if supported_adaptive_tx: + self.assertTrue(params['adaptive_tx']) + def test_ethtool_flow_control(self): for interface in self._interfaces: # Disable flow-control @@ -227,7 +316,7 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): self.cli_commit() for interface in self._interfaces: - frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') + frrconfig = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(' evpn mh uplink', frrconfig) def test_switchdev(self): @@ -240,4 +329,4 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): self.cli_delete(self._base_path + [interface, 'switchdev']) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_interfaces_geneve.py b/smoketest/scripts/cli/test_interfaces_geneve.py index 5f8fae91e..d8d28e346 100755 --- a/smoketest/scripts/cli/test_interfaces_geneve.py +++ b/smoketest/scripts/cli/test_interfaces_geneve.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -20,6 +20,7 @@ from vyos.ifconfig import Interface from vyos.utils.network import get_interface_config from base_interfaces_test import BasicInterfaceTest +from base_vyostest_shim import VyOSUnitTestSHIM class GeneveInterfaceTest(BasicInterfaceTest.TestCase): @classmethod @@ -81,4 +82,4 @@ class GeneveInterfaceTest(BasicInterfaceTest.TestCase): ttl += 10 if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_interfaces_input.py b/smoketest/scripts/cli/test_interfaces_input.py index 3ddf86000..fc1a3bdb8 100755 --- a/smoketest/scripts/cli/test_interfaces_input.py +++ b/smoketest/scripts/cli/test_interfaces_input.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,9 +16,10 @@ import unittest +from base_vyostest_shim import VyOSUnitTestSHIM + from vyos.utils.file import read_file from vyos.ifconfig import Interface -from base_vyostest_shim import VyOSUnitTestSHIM base_path = ['interfaces', 'input'] @@ -32,6 +33,8 @@ class InputInterfaceTest(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.cli_delete(base_path) self.cli_commit() + # always forward to base class + super().tearDown() def test_01_description(self): # Check if PPPoE dialer can be configured and runs @@ -48,4 +51,4 @@ class InputInterfaceTest(VyOSUnitTestSHIM.TestCase): self.assertEqual(Interface(interface).get_alias(), f'foo-{interface}') if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_interfaces_l2tpv3.py b/smoketest/scripts/cli/test_interfaces_l2tpv3.py index 28165736b..cbe1c85a8 100755 --- a/smoketest/scripts/cli/test_interfaces_l2tpv3.py +++ b/smoketest/scripts/cli/test_interfaces_l2tpv3.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,8 +18,11 @@ import json import unittest from base_interfaces_test import BasicInterfaceTest +from base_vyostest_shim import VyOSUnitTestSHIM + from vyos.utils.process import cmd from vyos.utils.kernel import unload_kmod + class L2TPv3InterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): @@ -63,4 +66,4 @@ if __name__ == '__main__': 'l2tp_netlink', 'l2tp_core']: unload_kmod(module) - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_interfaces_loopback.py b/smoketest/scripts/cli/test_interfaces_loopback.py index 0454dc658..f3a3602d1 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 VyOS maintainers and contributors <maintainers@vyos.io> # # 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,11 +15,15 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import unittest +from netifaces import interfaces # pylint: disable = no-name-in-module from base_interfaces_test import BasicInterfaceTest -from netifaces import interfaces +from base_interfaces_test import MSG_TESTCASE_UNSUPPORTED +from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.frrender import mgmt_daemon from vyos.utils.network import is_intf_addr_assigned +from vyos.utils.process import process_named_running loopbacks = ['127.0.0.1', '::1'] @@ -42,6 +46,9 @@ class LoopbackInterfaceTest(BasicInterfaceTest.TestCase): for intf in self._interfaces: self.assertIn(intf, interfaces()) + # check process health and continuity + self.assertEqual(self.mgmt_daemon_pid, process_named_running(mgmt_daemon)) + def test_add_single_ip_address(self): super().test_add_single_ip_address() for addr in loopbacks: @@ -53,7 +60,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) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_interfaces_macsec.py b/smoketest/scripts/cli/test_interfaces_macsec.py index d73895b7f..85ac9b795 100755 --- a/smoketest/scripts/cli/test_interfaces_macsec.py +++ b/smoketest/scripts/cli/test_interfaces_macsec.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,6 +18,7 @@ import re import unittest from base_interfaces_test import BasicInterfaceTest +from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section @@ -49,7 +50,9 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase): super(MACsecInterfaceTest, cls).setUpClass() def tearDown(self): + # always forward to base class super().tearDown() + self.assertFalse(process_named_running(PROCESS_NAME)) def test_macsec_encryption(self): @@ -269,4 +272,4 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase): self.assertTrue(tmp['linkinfo']['info_data']['encrypt']) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_interfaces_openvpn.py b/smoketest/scripts/cli/test_interfaces_openvpn.py index e087b8735..2cf3a26c0 100755 --- a/smoketest/scripts/cli/test_interfaces_openvpn.py +++ b/smoketest/scripts/cli/test_interfaces_openvpn.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,7 +19,7 @@ import unittest from glob import glob from ipaddress import IPv4Network -from netifaces import interfaces +from netifaces import interfaces # pylint: disable = no-name-in-module from base_vyostest_shim import VyOSUnitTestSHIM @@ -118,6 +118,9 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() + # always forward to base class + super().tearDown() + def test_openvpn_client_verify(self): # Create OpenVPN client interface and test verify() steps. interface = 'vtun2000' @@ -287,7 +290,7 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.assertIn(f'remote {remote_host}', config) self.assertIn(f'persist-tun', config) - # IPv4 only: client usees udp4 protocol + # IPv4 only: client uses udp4 protocol self.cli_set(path + ['ip-version', 'ipv4']) self.cli_commit() @@ -316,7 +319,7 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): interface = 'vtun5000' path = base_path + [interface] - # check validate() - must speciy operating mode + # check validate() - must specify operating mode self.cli_set(path) with self.assertRaises(ConfigSessionError): self.cli_commit() @@ -556,7 +559,7 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.assertIn(f'lport {port}', config) self.assertIn(f'push "redirect-gateway def1"', config) - # IPv4 only: server usees udp4 protocol + # IPv4 only: server uses udp4 protocol self.cli_set(path + ['ip-version', 'ipv4']) self.cli_commit() @@ -639,6 +642,12 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.cli_commit() self.cli_set(path + ['shared-secret-key', 'ovpn_test']) + # check validate() - Must define "encryption cipher" or "encryption + # data-ciphers-fallback" for site-to-site encryption + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(path + ['encryption', 'cipher', '3des']) + self.cli_commit() def test_openvpn_options(self): @@ -790,7 +799,7 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.assertIn(f'lport {port}', config) self.assertIn(f'rport {port}', config) - # IPv4 only: server usees udp4 protocol + # IPv4 only: server uses udp4 protocol self.cli_set(path + ['ip-version', 'ipv4']) self.cli_commit() @@ -813,6 +822,30 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() + def test_site2site_data_ciphers_fallback(self): + vtun_if = 'vtun2010' + path = ['interfaces', 'openvpn', vtun_if] + + # Configure a minimal site-to-site tunnel with data-ciphers-fallback + self.cli_set(path + ['mode', 'site-to-site']) + self.cli_set(path + ['encryption', 'data-ciphers-fallback', 'aes192']) + self.cli_set(path + ['local-address', '10.0.1.1']) + self.cli_set(path + ['remote-address', '10.0.1.2']) + self.cli_set(path + ['shared-secret-key', 'ovpn_test']) + + self.cli_commit() + + config_file = f'/run/openvpn/{vtun_if}.conf' + config = read_file(config_file) + + # Validate correct OpenVPN configuration rendering + self.assertIn(f'dev {vtun_if}', config) + self.assertIn('data-ciphers-fallback AES-192-CBC', config) + + # Ensure no other directives are rendered + self.assertNotIn('cipher ', config) + self.assertNotIn('data-ciphers ', config) + def test_openvpn_server_server_bridge(self): # Create OpenVPN server interface using bridge. # Validate configuration afterwards. @@ -826,7 +859,6 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): gw_subnet = "192.168.0.1" self.cli_set(['interfaces', 'bridge', br_if, 'member', 'interface', vtun_if]) - self.cli_set(path + ['device-type', 'tap']) self.cli_set(path + ['encryption', 'data-ciphers', 'aes192']) self.cli_set(path + ['hash', auth_hash]) self.cli_set(path + ['mode', 'server']) @@ -840,6 +872,10 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.cli_set(path + ['tls', 'certificate', 'ovpn_test']) self.cli_set(path + ['tls', 'dh-params', 'ovpn_test']) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(path + ['device-type', 'tap']) self.cli_commit() config_file = f'/run/openvpn/{vtun_if}.conf' @@ -865,4 +901,4 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.cli_commit() if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_interfaces_pppoe.py b/smoketest/scripts/cli/test_interfaces_pppoe.py index 2683a3122..b8b0429a8 100755 --- a/smoketest/scripts/cli/test_interfaces_pppoe.py +++ b/smoketest/scripts/cli/test_interfaces_pppoe.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,41 @@ import unittest from psutil import process_iter +from ipaddress import IPv4Address +from ipaddress import IPv6Address +from ipaddress import IPv4Network +from ipaddress import IPv6Network from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError +from vyos.utils.dict import dict_search_recursive +from vyos.utils.network import get_interface_address from vyos.xml_ref import default_value -config_file = '/etc/ppp/peers/{}' -base_path = ['interfaces', 'pppoe'] +config_file: str = '/etc/ppp/peers/{}' +base_path: list = ['interfaces', 'pppoe'] +veth_path: list = ['interfaces', 'virtual-ethernet'] +pppoe_server_path = ['service', 'pppoe-server'] +connect_timeout: int = 20 +name_servers: list = ['1.1.1.1', '2.2.2.2'] +ipv4_pool: str = '100.64.0.0/18' +ipv6_pool: str = '2001:db8:8000::/48' +ipv6_pool_pd: str = '2001:db8:9000::/48' + +def calculate_ipv6_interface_address(prefix: IPv6Network, sla_id: int, interface_id: int): + # Ensure SLA-ID is 8 bits + if not (0 <= sla_id <= 0xFF): + raise ValueError('SLA-ID must be an 8-bit integer (0-255)') + + # Ensure Interface ID is 64 bits + if not (0 <= interface_id <= 0xFFFFFFFFFFFFFFFF): + raise ValueError('Interface ID must be a 64-bit integer') + + # Build the /64 subnet from the PD prefix len + SLA-ID + subnet_int = int(prefix.network_address) | (sla_id << 64) + + # Calculate full interface address + return IPv6Address(subnet_int | interface_id) def get_config_value(interface, key): with open(config_file.format(interface), 'r') as f: @@ -32,6 +60,19 @@ def get_config_value(interface, key): return list(line.split()) return [] +def wait_for_interface(interface: str, timeout=connect_timeout) -> bool: + """ Wait until PPPoE interface has been connected to the BRAS """ + from time import time + from time import sleep + from vyos.utils.network import get_interface_config + + start_time = time() + while not get_interface_config(interface): + sleep(0.250) + if time() - start_time >= timeout: + return False + return True + # add a classmethod to setup a temporaray PPPoE server for "proper" validation class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): @classmethod @@ -40,29 +81,79 @@ class PPPoEInterfaceTest(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, veth_path) + cls.cli_delete(cls, pppoe_server_path) cls._interfaces = ['pppoe10', 'pppoe20', 'pppoe30'] - cls._source_interface = 'eth0' + cls._source_interface = 'veth102' + pppoe_server_interface = 'veth101' + + cls.cli_set(cls, veth_path + [pppoe_server_interface, 'peer-name', cls._source_interface]) + cls.cli_set(cls, veth_path + [cls._source_interface, 'peer-name', pppoe_server_interface]) + + cls.cli_set(cls, pppoe_server_path + ['authentication', 'mode', 'local']) + cls.cli_set(cls, pppoe_server_path + ['client-ip-pool', 'IPv4-POOL', 'range', ipv4_pool]) + cls.cli_set(cls, pppoe_server_path + ['client-ipv6-pool', 'IPv6-POOL', 'prefix', ipv6_pool, 'mask', '64']) + cls.cli_set(cls, pppoe_server_path + ['client-ipv6-pool', 'IPv6-POOL', 'delegate', ipv6_pool_pd, 'delegation-prefix', '56']) + cls.cli_set(cls, pppoe_server_path + ['default-ipv6-pool', 'IPv6-POOL']) + cls.cli_set(cls, pppoe_server_path + ['default-pool', 'IPv4-POOL']) + cls.cli_set(cls, pppoe_server_path + ['gateway-address', '100.64.0.1']) + cls.cli_set(cls, pppoe_server_path + ['interface', pppoe_server_interface]) + for ns in name_servers: + cls.cli_set(cls, pppoe_server_path + ['name-server', ns]) + cls.cli_set(cls, pppoe_server_path + ['ppp-options', 'disable-ccp']) + cls.cli_set(cls, pppoe_server_path + ['ppp-options', 'ipv6', 'allow']) + cls.cli_set(cls, pppoe_server_path + ['session-control', 'disable']) + + cls.u_p_dict = {} + for interface in cls._interfaces: + username = f'VyOS-user-{interface}' + password = f'VyOS-passwd-{interface}' + + cls.cli_set(cls, pppoe_server_path + ['authentication', 'local-users', + 'username', username, 'password', password]) + + cls.u_p_dict[interface] = (username, password) + + # Start PPPoE server + cls.cli_commit(cls) - def tearDown(self): - # Validate PPPoE client process - for interface in self._interfaces: - running = False - for proc in process_iter(): - if interface in proc.cmdline(): - running = True - break - self.assertTrue(running) + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, base_path) + cls.cli_delete(cls, veth_path) + cls.cli_delete(cls, pppoe_server_path) + # Stop PPPoE server + cls.cli_commit(cls) + + super(PPPoEInterfaceTest, cls).tearDownClass() + def tearDown(self): self.cli_delete(base_path) self.cli_commit() + # always forward to base class + super().tearDown() + + def _verify_interface_address(self, interface): + # Verify that the assigned IPv4/IPv6 addresses from the BRAS (PPPoE + # server) are from the assigned pools + for address in get_interface_address(interface): + if 'family' in address and address['family'] == 'inet': + # The PPPoE assigned IPv4 address must be from our pool + self.assertIn(IPv4Address(address['address']), IPv4Network(ipv4_pool)) + elif 'family' in address and address['family'] == 'inet6': + # The PPPoE assigned IPv6 address must be from our pool + ipv6 = IPv6Address(address['address']) + if not ipv6.is_link_local: + self.assertIn(ipv6, IPv6Network(ipv6_pool)) + def test_pppoe_client(self): # Check if PPPoE dialer can be configured and runs + mtu = '1400' + for interface in self._interfaces: - user = f'VyOS-user-{interface}' - passwd = f'VyOS-passwd-{interface}' - mtu = '1400' + (user, passwd) = self.u_p_dict[interface] self.cli_set(base_path + [interface, 'authentication', 'username', user]) self.cli_set(base_path + [interface, 'authentication', 'password', passwd]) @@ -79,8 +170,10 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): # verify configuration file(s) for interface in self._interfaces: - user = f'VyOS-user-{interface}' - passwd = f'VyOS-passwd-{interface}' + self.assertTrue(wait_for_interface(interface), + msg=f'Interface {interface} not found after {connect_timeout} seconds!') + + (user, passwd) = self.u_p_dict[interface] tmp = get_config_value(interface, 'mtu')[1] self.assertEqual(tmp, mtu) @@ -94,11 +187,21 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): tmp = get_config_value(interface, 'ifname')[1] self.assertEqual(tmp, interface) + # Validate and verify assigned IP addresses + self._verify_interface_address(interface) + + # validate that we have learned a default route + tmp = self.getFRRopmode('show ip route 0.0.0.0/0', json=True) + # Test if we have a default route 0.0.0.0/0 pointing to our PPPoE interface + tmp = dict_search_recursive(tmp, 'interfaceName') + + #self.assertTrue(any(iface == interface for (iface, _) in tmp)) + self.skipTest('Bug in FRR 10.2 - PPPoE interfaces sometimes carry ifIndex 0 which is invalid') + def test_pppoe_client_disabled_interface(self): # Check if PPPoE Client can be disabled for interface in self._interfaces: - user = f'VyOS-user-{interface}' - passwd = f'VyOS-passwd-{interface}' + (user, passwd) = self.u_p_dict[interface] self.cli_set(base_path + [interface, 'authentication', 'username', user]) self.cli_set(base_path + [interface, 'authentication', 'password', passwd]) @@ -122,17 +225,16 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): self.cli_commit() - def test_pppoe_authentication(self): # When username or password is set - so must be the other for interface in self._interfaces: - user = f'VyOS-user-{interface}' - passwd = f'VyOS-passwd-{interface}' + (user, passwd) = self.u_p_dict[interface] + self.cli_set(base_path + [interface, 'address', 'dhcpv6']) self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) self.cli_set(base_path + [interface, 'ipv6', 'address', 'autoconf']) - self.cli_set(base_path + [interface, 'authentication', 'username', user]) + # check validate() - if user is set, so must be the password with self.assertRaises(ConfigSessionError): self.cli_commit() @@ -141,15 +243,21 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): self.cli_commit() + for interface in self._interfaces: + self.assertTrue(wait_for_interface(interface), + msg=f'Interface {interface} not found after {connect_timeout} seconds!') + + # Validate and verify assigned IP addresses + self._verify_interface_address(interface) + def test_pppoe_dhcpv6pd(self): # Check if PPPoE dialer can be configured with DHCPv6-PD - address = '1' - sla_id = '0' - sla_len = '8' + address = 1 + sla_id = 0xff for interface in self._interfaces: - user = f'VyOS-user-{interface}' - passwd = f'VyOS-passwd-{interface}' + (user, passwd) = self.u_p_dict[interface] + interface_id = ''.join(c for c in interface if c.isdigit()) self.cli_set(base_path + [interface, 'authentication', 'username', user]) self.cli_set(base_path + [interface, 'authentication', 'password', passwd]) @@ -158,18 +266,24 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) self.cli_set(base_path + [interface, 'ipv6', 'address', 'autoconf']) + # interface we will delegate to + delegate_if = f'dum{interface_id}' + self.cli_set(['interfaces', 'dummy', delegate_if]) + # prefix delegation stuff dhcpv6_pd_base = base_path + [interface, 'dhcpv6-options', 'pd', '0'] self.cli_set(dhcpv6_pd_base + ['length', '56']) - self.cli_set(dhcpv6_pd_base + ['interface', self._source_interface, 'address', address]) - self.cli_set(dhcpv6_pd_base + ['interface', self._source_interface, 'sla-id', sla_id]) + self.cli_set(dhcpv6_pd_base + ['interface', delegate_if, 'address'], value=str(address)) + self.cli_set(dhcpv6_pd_base + ['interface', delegate_if, 'sla-id'], value=str(sla_id)) # commit changes self.cli_commit() for interface in self._interfaces: - user = f'VyOS-user-{interface}' - passwd = f'VyOS-passwd-{interface}' + self.assertTrue(wait_for_interface(interface), + msg=f'Interface {interface} not found after {connect_timeout} seconds!') + + (user, passwd) = self.u_p_dict[interface] mtu_default = default_value(base_path + [interface, 'mtu']) tmp = get_config_value(interface, 'mtu')[1] @@ -181,43 +295,85 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): tmp = get_config_value(interface, '+ipv6 ipv6cp-use-ipaddr') self.assertListEqual(tmp, ['+ipv6', 'ipv6cp-use-ipaddr']) + # Validate and verify assigned IP addresses + self._verify_interface_address(interface) + + # interface we delegated to + delegate_if = f'dum{interface_id}' + tmp = get_interface_address(delegate_if) + self.assertIn('addr_info', tmp) + + # Verify IPv6 address received from out DHCPv6-PD + for addr_info in tmp['addr_info']: + if 'family' not in addr_info or addr_info['family'] != 'inet6': + continue + + # Skip link-local interface address + ipv6 = IPv6Address(addr_info['local']) + if ipv6.is_link_local: + continue + + # DHCPv6-PD assigned interface addres is of length /64 + self.assertEqual(addr_info['prefixlen'], 64) + # Interface IP address must be within the PD pool + self.assertIn(ipv6, IPv6Network(ipv6_pool_pd)) + # Get corresponding PD assigned prefix for this site/connection + pd_prefix = IPv6Network(f"{ipv6}/56", strict=False) + # Prefix must be within the PD pool + self.assertTrue(pd_prefix.subnet_of(IPv6Network(ipv6_pool_pd))) + + gen_addr = calculate_ipv6_interface_address(pd_prefix, sla_id, address) + self.assertEqual(gen_addr, ipv6) + + self.cli_delete(['interfaces', 'dummy', delegate_if]) + def test_pppoe_options(self): - # Check if PPPoE dialer can be configured with DHCPv6-PD - for interface in self._interfaces: - user = f'VyOS-user-{interface}' - passwd = f'VyOS-passwd-{interface}' - ac_name = f'AC{interface}' - service_name = f'SRV{interface}' - host_uniq = 'cafebeefBABE123456' + # Verify access-concentrator and service-name CLI options - self.cli_set(base_path + [interface, 'authentication', 'username', user]) - self.cli_set(base_path + [interface, 'authentication', 'password', passwd]) - self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) + ac_name: str = 'ACN123' + service_name: str = 'VyOS' + + self.cli_set(pppoe_server_path + ['access-concentrator', ac_name]) + self.cli_set(pppoe_server_path + ['service-name', service_name]) + self.cli_commit() + + # as this tests uniqueness - we only use one interface in this test + interface = self._interfaces[0] + (user, passwd) = self.u_p_dict[interface] + + host_uniq = 'cafe010203' - self.cli_set(base_path + [interface, 'access-concentrator', ac_name]) - self.cli_set(base_path + [interface, 'service-name', service_name]) - self.cli_set(base_path + [interface, 'host-uniq', host_uniq]) + self.cli_set(base_path + [interface, 'authentication', 'username', user]) + self.cli_set(base_path + [interface, 'authentication', 'password', passwd]) + self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) + + self.cli_set(base_path + [interface, 'access-concentrator', ac_name]) + self.cli_set(base_path + [interface, 'service-name', service_name]) + self.cli_set(base_path + [interface, 'host-uniq', host_uniq]) # commit changes self.cli_commit() - for interface in self._interfaces: - ac_name = f'AC{interface}' - service_name = f'SRV{interface}' - host_uniq = 'cafebeefBABE123456' + self.assertTrue(wait_for_interface(interface), + msg=f'Interface {interface} not found after {connect_timeout} seconds!') + + tmp = get_config_value(interface, 'pppoe-ac')[1] + self.assertEqual(tmp, f'"{ac_name}"') + tmp = get_config_value(interface, 'pppoe-service')[1] + self.assertEqual(tmp, f'"{service_name}"') + tmp = get_config_value(interface, 'pppoe-host-uniq')[1] + self.assertEqual(tmp, f'"{host_uniq}"') - tmp = get_config_value(interface, 'pppoe-ac')[1] - self.assertEqual(tmp, f'"{ac_name}"') - tmp = get_config_value(interface, 'pppoe-service')[1] - self.assertEqual(tmp, f'"{service_name}"') - tmp = get_config_value(interface, 'pppoe-host-uniq')[1] - self.assertEqual(tmp, f'"{host_uniq}"') + # Validate and verify assigned IP addresses + self._verify_interface_address(interface) + + self.cli_delete(pppoe_server_path + ['access-concentrator']) + self.cli_delete(pppoe_server_path + ['service-name']) def test_pppoe_mtu_mru(self): # Check if PPPoE dialer can be configured and runs for interface in self._interfaces: - user = f'VyOS-user-{interface}' - passwd = f'VyOS-passwd-{interface}' + (user, passwd) = self.u_p_dict[interface] mtu = '1400' mru = '1300' @@ -241,8 +397,10 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): # verify configuration file(s) for interface in self._interfaces: - user = f'VyOS-user-{interface}' - passwd = f'VyOS-passwd-{interface}' + self.assertTrue(wait_for_interface(interface), + msg=f'Interface {interface} not found after {connect_timeout} seconds!') + + (user, passwd) = self.u_p_dict[interface] tmp = get_config_value(interface, 'mtu')[1] self.assertEqual(tmp, mtu) @@ -255,5 +413,8 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): tmp = get_config_value(interface, 'ifname')[1] self.assertEqual(tmp, interface) + # Validate and verify assigned IP addresses + self._verify_interface_address(interface) + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_interfaces_pseudo-ethernet.py b/smoketest/scripts/cli/test_interfaces_pseudo-ethernet.py index 0d6f5bc1f..3be8e024f 100755 --- a/smoketest/scripts/cli/test_interfaces_pseudo-ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_pseudo-ethernet.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,10 @@ import os import unittest -from vyos.ifconfig import Section from base_interfaces_test import BasicInterfaceTest +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.ifconfig import Section class PEthInterfaceTest(BasicInterfaceTest.TestCase): @classmethod @@ -43,4 +45,4 @@ class PEthInterfaceTest(BasicInterfaceTest.TestCase): super(PEthInterfaceTest, cls).setUpClass() if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_interfaces_tunnel.py b/smoketest/scripts/cli/test_interfaces_tunnel.py index dd9f1d2d1..8d6d4c562 100755 --- a/smoketest/scripts/cli/test_interfaces_tunnel.py +++ b/smoketest/scripts/cli/test_interfaces_tunnel.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.utils.network import get_interface_config @@ -345,7 +346,7 @@ class TunnelInterfaceTest(BasicInterfaceTest.TestCase): if 'remote' in tunnel_config: self.cli_set(self._base_path + [tunnel, 'remote', tunnel_config['remote']]) - # GRE key must be supplied when two or more tunnels are formed to the same desitnation + # GRE key must be supplied when two or more tunnels are formed to the same destination with self.assertRaises(ConfigSessionError): self.cli_commit() for tunnel, tunnel_config in tunnels.items(): @@ -410,4 +411,4 @@ class TunnelInterfaceTest(BasicInterfaceTest.TestCase): self.cli_commit() if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_interfaces_virtual-ethernet.py b/smoketest/scripts/cli/test_interfaces_virtual-ethernet.py index c6a4613a7..96ca36830 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 VyOS maintainers and contributors <maintainers@vyos.io> # # 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,11 @@ import unittest -from netifaces import interfaces - -from vyos.utils.process import process_named_running from base_interfaces_test import BasicInterfaceTest +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSessionError +from vyos.utils.network import interface_exists class VEthInterfaceTest(BasicInterfaceTest.TestCase): @classmethod @@ -34,28 +35,20 @@ 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 - # interface. - def tearDown(self): - self.cli_delete(self._base_path) - self.cli_commit() + def test_invalid_peers(self): + peer = ('veth1001', 'veth1002') + self.cli_set(self._base_path + [peer[0]]) + self.cli_set(self._base_path + [peer[1], 'peer-name', peer[0]]) - # Verify that no previously interface remained on the system - for intf in self._interfaces: - self.assertNotIn(intf, interfaces()) + # Configuration mismatch between "veth1001" and "veth1001" + with self.assertRaises(ConfigSessionError): + self.cli_commit() - @classmethod - def tearDownClass(cls): - # No daemon started during tests should remain running - for daemon in ['dhcp6c', 'dhclient']: - cls.assertFalse(cls, process_named_running(daemon)) + self.cli_set(self._base_path + [peer[0], 'peer-name', peer[1]]) + self.cli_commit() - super(VEthInterfaceTest, cls).tearDownClass() + self.assertTrue(interface_exists(peer[0])) + self.assertTrue(interface_exists(peer[1])) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_interfaces_vti.py b/smoketest/scripts/cli/test_interfaces_vti.py index 8d90ca5ad..296d952d6 100755 --- a/smoketest/scripts/cli/test_interfaces_vti.py +++ b/smoketest/scripts/cli/test_interfaces_vti.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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_vyostest_shim import VyOSUnitTestSHIM from vyos.ifconfig import Interface from vyos.utils.network import is_intf_addr_assigned @@ -46,4 +47,4 @@ class VTIInterfaceTest(BasicInterfaceTest.TestCase): self.assertEqual(Interface(intf).get_admin_state(), 'down') if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_interfaces_vxlan.py b/smoketest/scripts/cli/test_interfaces_vxlan.py index 05900a4ba..b63fa79ca 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 VyOS maintainers and contributors <maintainers@vyos.io> # # 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,6 +16,9 @@ import unittest +from base_interfaces_test import BasicInterfaceTest +from base_vyostest_shim import VyOSUnitTestSHIM + from vyos.configsession import ConfigSessionError from vyos.ifconfig import Interface from vyos.ifconfig import Section @@ -25,8 +28,6 @@ from vyos.utils.network import interface_exists from vyos.utils.network import get_vxlan_vlan_tunnels from vyos.utils.network import get_vxlan_vni_filter from vyos.template import is_ipv6 -from vyos import ConfigError -from base_interfaces_test import BasicInterfaceTest def convert_to_list(ranges_to_convert): result_list = [] @@ -126,19 +127,17 @@ class VXLANInterfaceTest(BasicInterfaceTest.TestCase): 'source-interface eth0', 'vni 60' ] - params = [] for option in options: opts = option.split() - params.append(opts[0]) - self.cli_set(self._base_path + [ intf ] + opts) + self.cli_set(self._base_path + [intf] + opts) - with self.assertRaises(ConfigSessionError) as cm: + # verify() - Both group and remote cannot be specified + with self.assertRaises(ConfigSessionError): self.cli_commit() - exception = cm.exception - self.assertIn('Both group and remote cannot be specified', str(exception)) - for param in params: - self.cli_delete(self._base_path + [intf, param]) + # Remove blocking CLI option + self.cli_delete(self._base_path + [intf, 'group']) + self.cli_commit() def test_vxlan_external(self): @@ -390,4 +389,4 @@ class VXLANInterfaceTest(BasicInterfaceTest.TestCase): self.cli_delete(['interfaces', 'bridge', bridge]) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_interfaces_wireguard.py b/smoketest/scripts/cli/test_interfaces_wireguard.py index f8cd18cf2..cf98deda1 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-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,6 +18,8 @@ import os import unittest from base_interfaces_test import BasicInterfaceTest +from base_interfaces_test import VyOSUnitTestSHIM + from vyos.configsession import ConfigSessionError from vyos.utils.file import read_file from vyos.utils.process import cmd @@ -154,13 +156,15 @@ class WireGuardInterfaceTest(BasicInterfaceTest.TestCase): tmp = read_file(f'/sys/class/net/{intf}/threaded') self.assertTrue(tmp, "1") - def test_wireguard_peer_pubkey_change(self): + def test_wireguard_peer_change(self): # T5707 changing WireGuard CLI public key of a peer - it's not removed + # Also check if allowed-ips update - def get_peers(interface) -> list: + def get_peers(interface) -> list[tuple]: tmp = cmd(f'sudo wg show {interface} dump') first_line = True peers = [] + allowed_ips = [] for line in tmp.split('\n'): if not line: continue # Skip empty lines and last line @@ -170,24 +174,27 @@ class WireGuardInterfaceTest(BasicInterfaceTest.TestCase): first_line = False else: peers.append(items[0]) - return peers + allowed_ips.append(items[3]) + return peers, allowed_ips interface = 'wg1337' port = '1337' privkey = 'iJi4lb2HhkLx2KSAGOjji2alKkYsJjSPkHkrcpxgEVU=' pubkey_1 = 'srQ8VF6z/LDjKCzpxBzFpmaNUOeuHYzIfc2dcmoc/h4=' pubkey_2 = '8pbMHiQ7NECVP7F65Mb2W8+4ldGG2oaGvDSpSEsOBn8=' + allowed_ips_1 = '10.205.212.10/32' + allowed_ips_2 = '10.205.212.11/32' self.cli_set(base_path + [interface, 'address', '172.16.0.1/24']) self.cli_set(base_path + [interface, 'port', port]) self.cli_set(base_path + [interface, 'private-key', privkey]) self.cli_set(base_path + [interface, 'peer', 'VyOS', 'public-key', pubkey_1]) - self.cli_set(base_path + [interface, 'peer', 'VyOS', 'allowed-ips', '10.205.212.10/32']) + self.cli_set(base_path + [interface, 'peer', 'VyOS', 'allowed-ips', allowed_ips_1]) self.cli_commit() - peers = get_peers(interface) + peers, _ = get_peers(interface) self.assertIn(pubkey_1, peers) self.assertNotIn(pubkey_2, peers) @@ -196,10 +203,20 @@ class WireGuardInterfaceTest(BasicInterfaceTest.TestCase): self.cli_commit() # Verify config - peers = get_peers(interface) + peers, _ = get_peers(interface) self.assertNotIn(pubkey_1, peers) self.assertIn(pubkey_2, peers) + # Update allowed-ips + self.cli_delete(base_path + [interface, 'peer', 'VyOS', 'allowed-ips', allowed_ips_1]) + self.cli_set(base_path + [interface, 'peer', 'VyOS', 'allowed-ips', allowed_ips_2]) + self.cli_commit() + + # Verify config + _, allowed_ips = get_peers(interface) + self.assertNotIn(allowed_ips_1, allowed_ips) + self.assertIn(allowed_ips_2, allowed_ips) + def test_wireguard_hostname(self): # T4930: Test dynamic endpoint support interface = 'wg1234' @@ -236,5 +253,62 @@ class WireGuardInterfaceTest(BasicInterfaceTest.TestCase): # Ensure the service is no longer running after WireGuard interface is deleted self.assertFalse(is_systemd_service_running(domain_resolver)) + def test_wireguard_vrf_fwmark(self): + # T8509 Check fwmark ip rule created for WireGuard interface with VRF + interface = 'wg0' + port = '12345' + privkey = '6ISOkASm6VhHOOSz/5iIxw+Q9adq9zA17iMM4X40dlc=' + pubkey = 'n1CUsmR0M2LUUsyicBd6blZICwUqqWWHbu4ifZ2/9gk=' + mark = '101' + vrf_table = '200' + vrf = 'testvrf' + + 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]) + self.cli_set(base_interface_path + ['port', port]) + + peer_base_path = base_interface_path + ['peer', 'VyOS'] + 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(base_interface_path + ['fwmark', mark]) + self.cli_set(base_interface_path + ['vrf', vrf]) + self.cli_set(['vrf', 'name', vrf, 'table', vrf_table]) + + self.cli_commit() + + hex_fwmark = hex(int(mark)) + + # Verify ip rule at priority 1998 routes fwmark-tagged packets into the VRF + tmp = cmd(f'ip rule show priority 1998') + self.assertIn(f'fwmark {hex_fwmark} lookup {vrf}', tmp) + + # Remove VRF from the interface — ip rule must be cleaned up + self.cli_delete(base_interface_path + ['vrf']) + self.cli_commit() + + tmp = cmd(f'ip rule show priority 1998') + self.assertNotIn(f'fwmark {hex_fwmark}', tmp) + + # Re-add VRF — ip rule must be re-created + self.cli_set(base_interface_path + ['vrf', vrf]) + self.cli_commit() + + tmp = cmd(f'ip rule show priority 1998') + self.assertIn(f'fwmark {hex_fwmark} lookup {vrf}', tmp) + + # Delete the interface entirely — ip rule must be removed + self.cli_delete(base_interface_path) + self.cli_commit() + + tmp = cmd(f'ip rule show priority 1998') + self.assertNotIn(f'fwmark {hex_fwmark}', tmp) + + self.cli_delete(['vrf', 'name', vrf]) + + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_interfaces_wireless.py b/smoketest/scripts/cli/test_interfaces_wireless.py index b8b18f30f..a61c7740e 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 VyOS maintainers and contributors <maintainers@vyos.io> # # 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,9 +17,10 @@ import os import re import unittest +from glob import glob from base_interfaces_test import BasicInterfaceTest -from glob import glob +from base_interfaces_test import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.utils.file import read_file @@ -64,13 +65,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 +638,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) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_load-balancing_haproxy.py b/smoketest/scripts/cli/test_load-balancing_haproxy.py index 077f1974f..9cb031276 100755 --- a/smoketest/scripts/cli/test_load-balancing_haproxy.py +++ b/smoketest/scripts/cli/test_load-balancing_haproxy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,16 @@ # 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 time +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 call from vyos.utils.process import process_named_running from vyos.utils.file import read_file @@ -131,7 +136,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 @@ -145,15 +168,18 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): # Process must be terminated after deleting the config self.assertFalse(process_named_running(PROCESS_NAME)) + # always forward to base class + super().tearDown() + 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,20 +193,21 @@ 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' rule_twenty = '20' rule_thirty = '30' + rule_forty = '40' send_proxy = 'send-proxy' max_connections = '1000' @@ -195,6 +222,9 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['service', frontend, 'rule', rule_twenty, 'set', 'backend', bk_second_name]) self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'url-path', 'end', '/test']) self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'set', 'backend', bk_second_name]) + self.cli_set(base_path + ['service', frontend, 'rule', rule_forty, 'domain-name', domain_bk_second]) + self.cli_set(base_path + ['service', frontend, 'rule', rule_forty, 'set', 'backend', bk_second_name]) + self.cli_set(base_path + ['service', frontend, 'rule', rule_forty, 'wildcard-domain']) self.cli_set(back_base + [bk_first_name, 'mode', mode]) self.cli_set(back_base + [bk_first_name, 'server', bk_first_name, 'address', bk_server_first]) @@ -227,6 +257,8 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.assertIn(f'use_backend {bk_second_name} if {rule_twenty}', config) self.assertIn(f'acl {rule_thirty} path -i -m end /test', config) self.assertIn(f'use_backend {bk_second_name} if {rule_thirty}', config) + self.assertIn(f'acl {rule_forty} hdr(host) -i -m end .{domain_bk_second}', config) + self.assertIn(f'use_backend {bk_second_name} if {rule_forty}', config) # Backend self.assertIn(f'backend {bk_first_name}', config) @@ -241,9 +273,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 +285,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 +307,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 +349,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 +360,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 +371,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 +422,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 +490,67 @@ 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_tcp_health_checks_custom_port(self): + # Define variables + service = 'my-tcp-api' + mode = 'tcp' + front_port = '9000' + backend = 'bk-01' + balance = 'round-robin' + servers = [ + ('srv01', '192.0.2.11', '9001', '9011'), + ('srv02', '192.0.2.12', '9002', None), + ('srv03', '192.0.2.13', '9003', '9013'), + ] + + # Configure frontend + self.cli_set(base_path + ['service', service, 'backend', backend]) + self.cli_set(base_path + ['service', service, 'mode', mode]) + self.cli_set(base_path + ['service', service, 'port', front_port]) + + # Configure backend + self.cli_set(base_path + ['backend', backend, 'balance', balance]) + self.cli_set(base_path + ['backend', backend, 'mode', mode]) + + # Configure backend servers + for name, addr, port, check_port in servers: + base_server_path = base_path + ['backend', backend, 'server', name] + self.cli_set(base_server_path + ['address', addr]) + self.cli_set(base_server_path + ['port', port]) + + if check_port: + self.cli_set(base_server_path + ['check', 'port', check_port]) + else: + self.cli_set(base_server_path + ['check']) + + # Commit and read config + self.cli_commit() + config = read_file(HAPROXY_CONF) + config_lines = [line.strip() for line in config.splitlines()] + + # Validate Frontend + self.assertIn(f'frontend {service}', config) + self.assertIn(f'bind [::]:{front_port} v4v6', config) + self.assertIn(f'mode {mode}', config) + self.assertIn(f'default_backend {backend}', config) + + # Validate Backend + self.assertIn(f'backend {backend}', config) + self.assertIn('balance roundrobin', config) + self.assertIn(f'mode {mode}', config) + + # Validate backend servers + for name, addr, port, check_port in servers: + with self.subTest(name=name): + expected_line = f'server {name} {addr}:{port}' + if check_port: + expected_line += f' check port {check_port}' + else: + expected_line += f' check' + + self.assertIn(expected_line, config_lines) + + def test_reverse_proxy_logging(self): # Setup base self.base_config() self.cli_commit() @@ -477,7 +569,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 +580,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,16 +590,17 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.assertIn('log /dev/log local5 notice', config) self.assertIn('log /dev/log local6 crit', config) - def test_10_lb_reverse_proxy_http_compression(self): + def test_reverse_proxy_http_compression(self): # Setup base self.configure_pki() self.base_config() # Configure compression in frontend - self.cli_set(base_path + ['service', 'https_front', 'http-compression', 'algorithm', 'gzip']) - self.cli_set(base_path + ['service', 'https_front', 'http-compression', 'mime-type', 'text/html']) - self.cli_set(base_path + ['service', 'https_front', 'http-compression', 'mime-type', 'text/javascript']) - self.cli_set(base_path + ['service', 'https_front', 'http-compression', 'mime-type', 'text/plain']) + 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 @@ -517,11 +610,11 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): 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', 'https_front', 'http-compression', 'mime-type']) + self.cli_delete(base_path + ['service', haproxy_service_name, 'http-compression', 'mime-type']) with self.assertRaises(ConfigSessionError) as e: self.cli_commit() - def test_11_lb_haproxy_timeout(self): + def test_reverse_proxy_timeout(self): t_default_check = '5' t_default_client = '50' t_default_connect = '10' @@ -551,7 +644,7 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): 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', 'https_front', 'timeout', 'client', t_front_client]) + self.cli_set(base_path + ['service', haproxy_service_name, 'timeout', 'client', t_front_client]) self.cli_commit() @@ -569,5 +662,72 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): 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]) + + def test_reverse_proxy_listen_address_no_port_conflict(self): + # HAProxy port conflict check must consider listen-address (T7928) + + frontend = 'svc-1' + backend = 'bk-1' + shared_port = '993' + addr_listen = '::1' # HAProxy listen-address + addr_busy = '127.0.0.1' # IP address kept busy by nc (different from the first) + + # Run the netcat command to bind to the specified address and port + call(f'sudo nc -lk -s {addr_busy} -p {shared_port} &') + + # Give nc a moment to bind before we commit + time.sleep(0.5) + + try: + backend_path = base_path + ['backend', backend] + self.cli_set(backend_path + ['mode', 'tcp']) + self.cli_set(backend_path + ['server', 'srv-m', 'address', '192.0.2.14']) + self.cli_set(backend_path + ['server', 'srv-m', 'port', shared_port]) + + # Configure HAProxy frontend with listen-address + service_path = base_path + ['service', frontend] + self.cli_set(service_path + ['mode', 'tcp']) + self.cli_set(service_path + ['port', shared_port]) + self.cli_set(service_path + ['listen-address', addr_listen]) + self.cli_set(service_path + ['backend', backend]) + + # Must commit without raising "TCP port N is used by another service" + self.cli_commit() + + # Commit with raising "TCP port N is used by another service" + self.cli_set(service_path + ['listen-address', addr_busy]) + with self.assertRaises(ConfigSessionError) as e: + self.cli_commit() + finally: + # Always clean up nc regardless of test outcome + call('sudo pkill nc') + + self.assertTrue(process_named_running(PROCESS_NAME)) + config = read_file(HAPROXY_CONF) + + # The busy address must NOT appear as a HAProxy bind + self.assertNotIn(f'bind {addr_busy}:{shared_port}', config) + self.assertIn(f'bind [{addr_listen}]:{shared_port}', config) + + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_load-balancing_wan.py b/smoketest/scripts/cli/test_load-balancing_wan.py index 32e5f6915..1692b8760 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-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,8 +21,10 @@ 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.misc import wait_for from vyos.utils.process import call from vyos.utils.process import cmd +from vyos.utils.process import rc_cmd base_path = ['load-balancing'] @@ -67,6 +69,9 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): for chain in removed_chains: self.verify_nftables_chain_exists('ip vyos_wanloadbalance', chain, inverse=True) + # always forward to base class + super().tearDown() + def test_table_routes(self): ns1 = 'ns201' ns2 = 'ns202' @@ -133,6 +138,23 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): tmp = cmd('sudo ip route show table 202') self.assertEqual(tmp, original) + tmp = cmd('sudo ip rule show') + self.assertIn('from all fwmark 0xc9 lookup 201', tmp) + self.assertIn('from all fwmark 0xca lookup 202', tmp) + self.assertNotIn('fwmark 0xc9 lookup main suppress_prefixlength 0', tmp) + self.assertNotIn('fwmark 0xca lookup main suppress_prefixlength 0', tmp) + + self.cli_set(base_path + ['wan', 'only-default-route']) + self.cli_commit() + + time.sleep(5) + + tmp = cmd('sudo ip rule show') + self.assertIn('from all fwmark 0xc9 lookup main suppress_prefixlength 0', tmp) + self.assertIn('from all fwmark 0xc9 lookup 201', tmp) + self.assertIn('from all fwmark 0xca lookup main suppress_prefixlength 0', tmp) + self.assertIn('from all fwmark 0xca lookup 202', tmp) + # Delete veth interfaces and netns for iface in [iface1, iface2, iface3]: call(f'sudo ip link del dev {iface}') @@ -164,15 +186,15 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): mangle_prerouting = """table ip vyos_wanloadbalance { chain wlb_mangle_prerouting { type filter hook prerouting priority mangle; policy accept; - 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 ct state new 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 vyos_wanloadbalance { chain wlb_nat_postrouting { type nat hook postrouting priority srcnat - 1; policy accept; - ct mark 0x000000c9 counter snat to 203.0.113.10 - ct mark 0x000000ca counter snat to 192.0.2.10 + ct mark 0x000000c9 oifname "veth1" counter snat to 203.0.113.10 + ct mark 0x000000ca oifname "veth2" counter snat to 192.0.2.10 } }""" @@ -234,6 +256,27 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): tmp = cmd('sudo nft -s list chain ip vyos_wanloadbalance wlb_nat_postrouting') self.assertEqual(tmp, nat_wanloadbalance) + # Set limit configuration + mangle_prerouting_limit = """table ip vyos_wanloadbalance { + chain wlb_mangle_prerouting { + type filter hook prerouting priority mangle; policy accept; + iifname "veth3" ip saddr 198.51.100.0/24 ct state new limit rate 10/second burst 10 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 + } +}""" + + self.cli_set(base_path + ['wan', 'rule', '10', 'limit', 'rate', '10']) + self.cli_set(base_path + ['wan', 'rule', '10', 'limit', 'burst', '10']) + + # Commit changes + self.cli_commit() + + time.sleep(5) + + # Check prerouting mangle chain + tmp = cmd('sudo nft -s list chain ip vyos_wanloadbalance wlb_mangle_prerouting') + self.assertEqual(tmp, mangle_prerouting_limit) + # Delete veth interfaces and netns for iface in [iface1, iface2, iface3]: call(f'sudo ip link del dev {iface}') @@ -285,6 +328,17 @@ echo "$ifname - $state" > {hook_output_path} 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]) + self.cli_set(base_path + ['wan', 'rule', '20', 'inbound-interface', lan_iface]) + self.cli_set(base_path + ['wan', 'rule', '20', 'protocol', 'udp']) + self.cli_set( + base_path + ['wan', 'rule', '20', 'source', 'address', '198.51.100.0/24'] + ) + self.cli_set(base_path + ['wan', 'rule', '20', 'source', 'port', '80,443']) + self.cli_set( + base_path + ['wan', 'rule', '20', 'destination', 'address', '192.0.2.0/24'] + ) + self.cli_set(base_path + ['wan', 'rule', '20', 'destination', 'port', '80,443']) + self.cli_set(base_path + ['wan', 'rule', '20', 'interface', isp2_iface]) # commit changes self.cli_commit() @@ -295,7 +349,22 @@ echo "$ifname - $state" > {hook_output_path} 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}'] + [ + 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}', + ], + [ + f'iifname "{lan_iface}"', + 'ip saddr 198.51.100.0/24', + 'udp sport { 80, 443 }', + 'ip daddr 192.0.2.0/24', + 'udp dport { 80, 443 }', + f'jump wlb_mangle_isp_{isp2_iface}', + ], ] self.verify_nftables_chain(nftables_search, 'ip vyos_wanloadbalance', 'wlb_mangle_prerouting') @@ -322,5 +391,133 @@ echo "$ifname - $state" > {hook_output_path} with open(hook_output_path, 'r') as f: self.assertIn('eth0 - FAILED', f.read()) + def test_firewall_groups(self): + isp1_iface = 'eth0' + isp2_iface = 'eth1' + lan_iface = 'eth2' + + network_group1 = 'NET1' + network_group2 = 'NET2' + port_group = 'PORT1' + + 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(['firewall', 'group', 'network-group', network_group1, 'network', '10.0.0.0/8']) + self.cli_set(['firewall', 'group', 'network-group', network_group2, 'network', '198.51.100.0/24']) + self.cli_set(['firewall', 'group', 'port-group', port_group, 'port', '53']) + + 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', 'group', 'network-group', network_group1]) + 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', 'group', 'network-group', network_group2]) + self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'group', 'port-group', port_group]) + self.cli_set(base_path + ['wan', 'rule', '10', 'destination', 'address', '192.0.2.0/24']) + self.cli_set(base_path + ['wan', 'rule', '10', 'destination', 'group', 'port-group', port_group]) + 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) + + nftables_search = [ + ['iifname "eth*"', f'ip daddr @N_{network_group1}', 'return'], + [ + f'iifname "{lan_iface}"', + f'ip saddr @N_{network_group2}', + f'udp sport @P_{port_group}', + 'ip daddr 192.0.2.0/24', + f'udp dport @P_{port_group}', + f'jump wlb_mangle_isp_{isp1_iface}', + ], + ] + + self.verify_nftables_chain(nftables_search, 'ip vyos_wanloadbalance', 'wlb_mangle_prerouting') + + def test_3_or_more_interfaces_in_rule(self): + lan_iface = 'eth1' + + # Interfaces for equal weight test + self.cli_set(['interfaces', 'ethernet', 'eth0', 'vif', '101', 'address', '203.0.113.2/30']) + self.cli_set(['interfaces', 'ethernet', 'eth0', 'vif', '102', 'address', '203.0.113.6/30']) + self.cli_set(['interfaces', 'ethernet', 'eth0', 'vif', '103', 'address', '203.0.113.10/30']) + + # Interfaces for unequal weight test + self.cli_set(['interfaces', 'ethernet', 'eth0', 'vif', '201', 'address', '203.0.113.14/30']) + self.cli_set(['interfaces', 'ethernet', 'eth0', 'vif', '202', 'address', '203.0.113.18/30']) + self.cli_set(['interfaces', 'ethernet', 'eth0', 'vif', '203', 'address', '203.0.113.22/30']) + self.cli_set(['interfaces', 'ethernet', 'eth0', 'vif', '204', 'address', '203.0.113.26/30']) + self.cli_set(['interfaces', 'ethernet', 'eth0', 'vif', '205', 'address', '203.0.113.30/30']) + + + self.cli_set(['interfaces', 'ethernet', lan_iface, 'vif', '100', 'address', '198.51.100.2/30']) + self.cli_set(['interfaces', 'ethernet', lan_iface, 'vif', '200', 'address', '198.51.100.6/30']) + + # Health checks for equal weight test + self.cli_set(base_path + ['wan', 'interface-health', 'eth0.101', 'nexthop', '203.0.113.2']) + self.cli_set(base_path + ['wan', 'interface-health', 'eth0.102', 'nexthop', '203.0.113.6']) + self.cli_set(base_path + ['wan', 'interface-health', 'eth0.103', 'nexthop', '203.0.113.10']) + self.cli_set(base_path + ['wan', 'rule', '10', 'inbound-interface', f'{lan_iface}.100']) + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', 'eth0.101']) + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', 'eth0.102']) + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', 'eth0.103']) + + # Health checks for unequal weight test + self.cli_set(base_path + ['wan', 'interface-health', 'eth0.201', 'nexthop', '203.0.113.14']) + self.cli_set(base_path + ['wan', 'interface-health', 'eth0.202', 'nexthop', '203.0.113.18']) + self.cli_set(base_path + ['wan', 'interface-health', 'eth0.203', 'nexthop', '203.0.113.22']) + self.cli_set(base_path + ['wan', 'interface-health', 'eth0.204', 'nexthop', '203.0.113.26']) + self.cli_set(base_path + ['wan', 'interface-health', 'eth0.205', 'nexthop', '203.0.113.30']) + self.cli_set(base_path + ['wan', 'rule', '20', 'inbound-interface', f'{lan_iface}.200']) + self.cli_set(base_path + ['wan', 'rule', '20', 'interface', 'eth0.201', 'weight', '2']) + self.cli_set(base_path + ['wan', 'rule', '20', 'interface', 'eth0.202', 'weight', '4']) + self.cli_set(base_path + ['wan', 'rule', '20', 'interface', 'eth0.203', 'weight', '4']) + self.cli_set(base_path + ['wan', 'rule', '20', 'interface', 'eth0.204', 'weight', '7']) + self.cli_set(base_path + ['wan', 'rule', '20', 'interface', 'eth0.205', 'weight', '4']) + + # commit changes + self.cli_commit() + + def check_wlb_status(): + rc, wlb_status = rc_cmd('nft list chain ip vyos_wanloadbalance wlb_mangle_prerouting') + if rc != 0: + return False + + # get all lines containing 'jump' + lines = [l for l in wlb_status.splitlines() if 'jump' in l] + + # check total count of 'jump' across all matching lines + total_jumps = sum(l.count('jump') for l in lines) + + return total_jumps == 8 + + wait_for(check_wlb_status) + + nftables_search = [ + ['0 : jump wlb_mangle_isp_eth0.101', + '1 : jump wlb_mangle_isp_eth0.102', + '2 : jump wlb_mangle_isp_eth0.103'], + ['0-1 : jump wlb_mangle_isp_eth0.201', + '2-5 : jump wlb_mangle_isp_eth0.202', + '6-9 : jump wlb_mangle_isp_eth0.203', + '10-13 : jump wlb_mangle_isp_eth0.205', + '14-20 : jump wlb_mangle_isp_eth0.204'], + ] + + self.verify_nftables_chain(nftables_search, 'ip vyos_wanloadbalance', 'wlb_mangle_prerouting') + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_nat.py b/smoketest/scripts/cli/test_nat.py index b33ef2617..a7669971d 100755 --- a/smoketest/scripts/cli/test_nat.py +++ b/smoketest/scripts/cli/test_nat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,7 +18,10 @@ import os import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from time import sleep + from vyos.configsession import ConfigSessionError +from vyos.utils.process import run base_path = ['nat'] src_path = base_path + ['source'] @@ -42,6 +45,8 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): self.cli_commit() self.assertFalse(os.path.exists(nftables_nat_config)) self.assertFalse(os.path.exists(nftables_static_nat_conf)) + # always forward to base class + super().tearDown() def wait_for_domain_resolver(self, table, set_name, element, max_wait=10): # Resolver no longer blocks commit, need to wait for daemon to populate set @@ -331,4 +336,4 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip vyos_nat') if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_nat64.py b/smoketest/scripts/cli/test_nat64.py index 5c907f6cb..dc0eb0abc 100755 --- a/smoketest/scripts/cli/test_nat64.py +++ b/smoketest/scripts/cli/test_nat64.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -20,6 +20,9 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.configsession import ConfigSessionError +from vyos.utils.system import sysctl_read + base_path = ['nat64'] src_path = base_path + ['source'] @@ -38,6 +41,8 @@ class TestNAT64(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() self.assertFalse(os.path.exists(jool_nat64_config)) + # always forward to base class + super().tearDown() def test_snat64(self): rule = '100' @@ -46,15 +51,25 @@ class TestNAT64(VyOSUnitTestSHIM.TestCase): pool = '192.0.2.10' pool_port = '1-65535' - self.cli_set(src_path + ['rule', rule, 'source', 'prefix', prefix_v6]) - self.cli_set( - src_path - + ['rule', rule, 'translation', 'pool', translation_rule, 'address', pool] - ) - self.cli_set( - src_path - + ['rule', rule, 'translation', 'pool', translation_rule, 'port', pool_port] - ) + rule_path = src_path + ['rule', rule] + self.cli_set(rule_path + ['source', 'prefix', prefix_v6]) + self.cli_set(rule_path + ['translation', 'pool', translation_rule, + 'address', pool]) + self.cli_set(rule_path + ['translation', 'pool', translation_rule, + 'port', pool_port]) + + # pool_port overlaps with the Linux Kernel ephemeral port range + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + # https://nicmx.github.io/Jool/en/usr-flags-pool4.html#port-range + # Jool is incapable of ensuring pool4 does not intersect with other defined + # port ranges; this validation is the operator’s responsibility. + tmp = sysctl_read(['net', 'ipv4', 'ip_local_port_range']) + _, ephemeral_port_max = map(int, tmp.split()) + pool_port = f'{ephemeral_port_max +1}-{ephemeral_port_max +1000}' + self.cli_set(rule_path + ['translation', 'pool', translation_rule, + 'port', pool_port]) self.cli_commit() # Load the JSON file @@ -95,4 +110,4 @@ class TestNAT64(VyOSUnitTestSHIM.TestCase): if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_nat66.py b/smoketest/scripts/cli/test_nat66.py index 52ad8e3ef..8bec2f998 100755 --- a/smoketest/scripts/cli/test_nat66.py +++ b/smoketest/scripts/cli/test_nat66.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -36,6 +36,34 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.cli_delete(base_path) self.cli_commit() + # always forward to base class + super().tearDown() + + def test_firewall_group_dependence(self): + address_group = 'smoketest_addr_dependence' + address_group_member_1 = 'fc00::1' + address_group_member_2 = 'fc00::2' + translation_prefix = 'fc01::/64' + + # add an address group and set a nat66 rule with it + self.cli_set(['firewall', 'group', 'ipv6-address-group', address_group, 'address', address_group_member_1]) + self.cli_set(dst_path + ['rule', '1', 'destination', 'group', 'address-group', address_group]) + self.cli_set(src_path + ['rule', '1', 'translation', 'address', translation_prefix]) + # commit changes to build configuration file + self.cli_commit() + + # replace the member of the address group + self.cli_delete(['firewall', 'group', 'ipv6-address-group', address_group, 'address', address_group_member_1]) + self.cli_set(['firewall', 'group', 'ipv6-address-group', address_group, 'address', address_group_member_2]) + self.cli_commit() + + # verify that the new member is in the address group + nftables_search = [ + [f'set A6_{address_group}'], + [f'elements = {{ {address_group_member_2} }}'] + ] + + self.verify_nftables(nftables_search, 'ip6 vyos_nat') def test_source_nat66(self): source_prefix = 'fc00::/64' @@ -146,10 +174,13 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): address_group_member = 'fc00::1' network_group = 'smoketest_net' network_group_member = 'fc00::/64' + mac_group = 'smoketest_mac' + mac_group_member = '00:01:02:03:04:05' 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(['firewall', 'group', 'mac-group', mac_group, 'mac-address', mac_group_member]) self.cli_set(dst_path + ['rule', '1', 'destination', 'group', 'address-group', address_group]) self.cli_set(dst_path + ['rule', '1', 'translation', 'address', translation_prefix]) @@ -157,6 +188,9 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): self.cli_set(dst_path + ['rule', '2', 'destination', 'group', 'network-group', network_group]) self.cli_set(dst_path + ['rule', '2', 'translation', 'address', translation_prefix]) + self.cli_set(dst_path + ['rule', '3', 'source', 'group', 'mac-group', mac_group]) + self.cli_set(dst_path + ['rule', '3', 'translation', 'address', translation_prefix]) + self.cli_commit() nftables_search = [ @@ -165,7 +199,8 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): [f'set N6_{network_group}'], [f'elements = {{ {network_group_member} }}'], ['ip6 daddr', f'@A6_{address_group}', 'dnat prefix to fc01::/64'], - ['ip6 daddr', f'@N6_{network_group}', 'dnat prefix to fc01::/64'] + ['ip6 daddr', f'@N6_{network_group}', 'dnat prefix to fc01::/64'], + ['ether saddr', f'@M_{mac_group}', 'dnat prefix to fc01::/64'], ] self.verify_nftables(nftables_search, 'ip6 vyos_nat') @@ -227,6 +262,42 @@ 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' + mac_group = 'smoketest_mac' + mac_group_member = '00:01:02:03:04:05' + 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(['firewall', 'group', 'mac-group', mac_group, 'mac-address', mac_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_set(src_path + ['rule', '3', 'source', 'group', 'mac-group', mac_group]) + self.cli_set(src_path + ['rule', '3', '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'], + ['ether saddr', f'@M_{mac_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'. @@ -238,4 +309,4 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): self.cli_commit() if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_netns.py b/smoketest/scripts/cli/test_netns.py index 2ac603a69..89d22ef77 100755 --- a/smoketest/scripts/cli/test_netns.py +++ b/smoketest/scripts/cli/test_netns.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -35,7 +35,8 @@ class NetNSTest(VyOSUnitTestSHIM.TestCase): tmp = cmd('ip netns ls') self.assertFalse(tmp) - super(NetNSTest, self).tearDown() + # always forward to base class + super().tearDown() def test_netns_create(self): namespaces = ['mgmt', 'front', 'back'] @@ -76,4 +77,4 @@ class NetNSTest(VyOSUnitTestSHIM.TestCase): self.assertFalse(is_netns_interface(interface, netns)) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_op-mode_show.py b/smoketest/scripts/cli/test_op-mode_show.py index 62f8e88da..893441be4 100755 --- a/smoketest/scripts/cli/test_op-mode_show.py +++ b/smoketest/scripts/cli/test_op-mode_show.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -43,4 +43,4 @@ class TestOPModeShow(VyOSUnitTestSHIM.TestCase): self.assertIn('VRF is not configured', tmp) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_pki.py b/smoketest/scripts/cli/test_pki.py index 02beafb26..540dc8e9a 100755 --- a/smoketest/scripts/cli/test_pki.py +++ b/smoketest/scripts/cli/test_pki.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -163,6 +163,8 @@ class TestPKI(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.cli_delete(base_path) self.cli_commit() + # always forward to base class + super().tearDown() def test_valid_pki(self): # Valid CA @@ -251,4 +253,4 @@ class TestPKI(VyOSUnitTestSHIM.TestCase): self.cli_delete(['interfaces', 'ethernet', interface, 'eapol']) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_policy.py b/smoketest/scripts/cli/test_policy.py index 985097726..304bbdbc8 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-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,7 +17,6 @@ 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 @@ -33,12 +32,12 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): # 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() + # always forward to base class + super().tearDown() def test_access_list(self): acls = { @@ -124,7 +123,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_commit() - config = self.getFRRconfig('access-list', end='') + config = self.getFRRconfig('access-list', end_marker='') for acl, acl_config in acls.items(): for rule, rule_config in acl_config['rule'].items(): tmp = f'access-list {acl} seq {rule}' @@ -215,7 +214,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_commit() - config = self.getFRRconfig('ipv6 access-list', end='') + config = self.getFRRconfig('ipv6 access-list', end_marker='') for acl, acl_config in acls.items(): for rule, rule_config in acl_config['rule'].items(): tmp = f'ipv6 access-list {acl} seq {rule}' @@ -313,7 +312,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_commit() - config = self.getFRRconfig('bgp as-path access-list', end='') + config = self.getFRRconfig('bgp as-path access-list', end_marker='') for as_path, as_path_config in test_data.items(): if 'rule' not in as_path_config: continue @@ -371,7 +370,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_commit() - config = self.getFRRconfig('bgp community-list', end='') + config = self.getFRRconfig('bgp community-list', end_marker='') for comm_list, comm_list_config in test_data.items(): if 'rule' not in comm_list_config: continue @@ -429,7 +428,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_commit() - config = self.getFRRconfig('bgp extcommunity-list', end='') + config = self.getFRRconfig('bgp extcommunity-list', end_marker='') for comm_list, comm_list_config in test_data.items(): if 'rule' not in comm_list_config: continue @@ -494,7 +493,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_commit() - config = self.getFRRconfig('bgp large-community-list', end='') + config = self.getFRRconfig('bgp large-community-list', end_marker='') for comm_list, comm_list_config in test_data.items(): if 'rule' not in comm_list_config: continue @@ -572,7 +571,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_commit() - config = self.getFRRconfig('ip prefix-list', end='') + config = self.getFRRconfig('ip prefix-list', end_marker='') for prefix_list, prefix_list_config in test_data.items(): if 'rule' not in prefix_list_config: continue @@ -655,7 +654,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_commit() - config = self.getFRRconfig('ipv6 prefix-list', end='') + config = self.getFRRconfig('ipv6 prefix-list', end_marker='') for prefix_list, prefix_list_config in test_data.items(): if 'rule' not in prefix_list_config: continue @@ -706,10 +705,61 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_commit() - config = self.getFRRconfig('ip prefix-list', end='') + config = self.getFRRconfig('ip prefix-list', end_marker='') for rule in test_range: tmp = f'ip prefix-list {prefix_list} seq {rule} permit {prefix} le {rule}' self.assertIn(tmp, config) + + def test_prefix_list_ge_le_validation(self): + # FRR requires mask_length <= ge <= le for prefix-list rules + base = base_path + ['prefix-list', 'getest', 'rule', '10'] + base6 = base_path + ['prefix-list6', 'getest6', 'rule', '10'] + + # ge < mask_length should be rejected + self.cli_set(base + ['action', 'permit']) + self.cli_set(base + ['prefix', '192.0.2.0/24']) + self.cli_set(base + ['ge', '16']) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base + ['ge']) + + # le < mask_length should be rejected + self.cli_set(base + ['le', '16']) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base + ['le']) + + # ge > le should be rejected + self.cli_set(base + ['ge', '28']) + self.cli_set(base + ['le', '26']) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base + ['ge']) + self.cli_delete(base + ['le']) + + # valid ge <= le >= mask_length should commit + self.cli_set(base + ['ge', '25']) + self.cli_set(base + ['le', '28']) + self.cli_commit() + self.cli_delete(base_path + ['prefix-list', 'getest']) + + # same checks for prefix-list6 + self.cli_set(base6 + ['action', 'permit']) + self.cli_set(base6 + ['prefix', '2a06:9801:2c0::/44']) + self.cli_set(base6 + ['ge', '48']) + self.cli_set(base6 + ['le', '44']) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base6 + ['ge']) + self.cli_delete(base6 + ['le']) + + # valid IPv6 ge/le + self.cli_set(base6 + ['ge', '48']) + self.cli_set(base6 + ['le', '64']) + self.cli_commit() + self.cli_delete(base_path + ['prefix-list6', 'getest6']) + self.cli_commit() + def test_route_map_community_set(self): test_data = { "community-configuration": { @@ -843,7 +893,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.assertIn(name, config) if 'set' in rule_config: - #Check community + # Check community if 'community' in rule_config['set']: if 'none' in rule_config['set']['community']: tmp = f'set community none' @@ -856,7 +906,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): values = ' '.join(rule_config['set']['community']['add']) tmp = f'set community {values} additive' self.assertIn(tmp, config) - #Check large-community + # Check large-community if 'large-community' in rule_config['set']: if 'none' in rule_config['set']['large-community']: tmp = f'set large-community none' @@ -869,7 +919,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): values = ' '.join(rule_config['set']['large-community']['add']) tmp = f'set large-community {values} additive' self.assertIn(tmp, config) - #Check extcommunity + # Check extcommunity if 'extcommunity' in rule_config['set']: if 'none' in rule_config['set']['extcommunity']: tmp = 'set extcommunity none' @@ -938,6 +988,12 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): 'tag': tag, }, }, + '7' : { + 'action' : 'deny', + 'match' : { + 'rpki-comm-invalid': '', + }, + }, '10' : { 'action' : 'permit', 'match' : { @@ -946,6 +1002,12 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): 'rpki-not-found': '', }, }, + '12' : { + 'action' : 'permit', + 'match' : { + 'rpki-comm-not-found': '', + }, + }, '15' : { 'action' : 'permit', 'match' : { @@ -956,6 +1018,12 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): 'next' : '', }, }, + '17' : { + 'action' : 'permit', + 'match' : { + 'rpki-comm-valid': '', + }, + }, '20' : { 'action' : 'permit', 'match' : { @@ -994,14 +1062,13 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): 'peer' : peer, }, }, - - '31' : { - 'action' : 'permit', - 'match' : { - 'peer' : peerv6, + '31': { + 'action': 'permit', + 'match': { + 'peer': peerv6, + 'source-peer': peer, }, }, - '40' : { 'action' : 'permit', 'match' : { @@ -1262,12 +1329,20 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_set(path + ['rule', rule, 'match', 'origin', 'incomplete']) if 'peer' in rule_config['match']: self.cli_set(path + ['rule', rule, 'match', 'peer', rule_config['match']['peer']]) + if 'source-peer' in rule_config['match']: + self.cli_set(path + ['rule', rule, 'match', 'source-peer', rule_config['match']['source-peer']]) if 'rpki-invalid' in rule_config['match']: self.cli_set(path + ['rule', rule, 'match', 'rpki', 'invalid']) if 'rpki-not-found' in rule_config['match']: self.cli_set(path + ['rule', rule, 'match', 'rpki', 'notfound']) if 'rpki-valid' in rule_config['match']: self.cli_set(path + ['rule', rule, 'match', 'rpki', 'valid']) + if 'rpki-comm-invalid' in rule_config['match']: + self.cli_set(path + ['rule', rule, 'match', 'rpki-extcommunity', 'invalid']) + if 'rpki-comm-not-found' in rule_config['match']: + self.cli_set(path + ['rule', rule, 'match', 'rpki-extcommunity', 'notfound']) + if 'rpki-comm-valid' in rule_config['match']: + self.cli_set(path + ['rule', rule, 'match', 'rpki-extcommunity', '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']: @@ -1438,6 +1513,9 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): if 'peer' in rule_config['match']: tmp = f'match peer {rule_config["match"]["peer"]}' self.assertIn(tmp, config) + if 'source-peer' in rule_config['match']: + tmp = f'match src-peer {rule_config["match"]["source-peer"]}' + self.assertIn(tmp, config) if 'protocol' in rule_config['match']: tmp = f'match source-protocol {rule_config["match"]["protocol"]}' self.assertIn(tmp, config) @@ -1450,6 +1528,15 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): if 'rpki-valid' in rule_config['match']: tmp = f'match rpki valid' self.assertIn(tmp, config) + if 'rpki-comm-invalid' in rule_config['match']: + tmp = f'match rpki-extcommunity invalid' + self.assertIn(tmp, config) + if 'rpki-comm-not-found' in rule_config['match']: + tmp = f'match rpki-extcommunity notfound' + self.assertIn(tmp, config) + if 'rpki-comm-valid' in rule_config['match']: + tmp = f'match rpki-extcommunity 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) @@ -1972,7 +2059,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='', endsection='^exit') + config = self.getFRRconfig(f'route-map {route_map} permit {seq}', stop_section='^exit') self.assertIn(f' set local-preference {local_preference}', config) self.assertIn(f' set table {table}', config) local_preference += 20 @@ -1985,7 +2072,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='', endsection='^exit') + config = self.getFRRconfig(f'route-map {route_map} permit {seq}', stop_section='^exit') self.assertIn(f' set local-preference {local_preference}', config) local_preference += 20 @@ -1999,7 +2086,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='', endsection='^exit') + config = self.getFRRconfig(f'route-map {route_map} permit {seq}', stop_section='^exit') self.assertIn(f' set as-path prepend {prepend}', config) for route_map in route_maps: @@ -2008,7 +2095,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='', endsection='^exit') + config = self.getFRRconfig(f'route-map {route_map} permit {seq}', stop_section='^exit') self.assertNotIn(f' set', config) def sort_ip(output): @@ -2018,4 +2105,4 @@ def sort_ip(output): return o if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_policy_local-route.py b/smoketest/scripts/cli/test_policy_local-route.py index a4239b8a1..a127f38cd 100644 --- a/smoketest/scripts/cli/test_policy_local-route.py +++ b/smoketest/scripts/cli/test_policy_local-route.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,7 +17,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from base_vyostest_shim import CSTORE_GUARD_TIME interface = 'eth0' mark = '100' @@ -33,8 +32,6 @@ class TestPolicyLocalRoute(VyOSUnitTestSHIM.TestCase): # 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]) @@ -55,6 +52,8 @@ class TestPolicyLocalRoute(VyOSUnitTestSHIM.TestCase): self.verify_rules(ip_rule_search, inverse=True) self.verify_rules(ip_rule_search, inverse=True, addr_family='inet6') + # always forward to base class + super().tearDown() def test_local_pbr_matching_criteria(self): self.cli_set(['policy', 'local-route', 'rule', '4', 'inbound-interface', interface]) @@ -171,4 +170,4 @@ class TestPolicyLocalRoute(VyOSUnitTestSHIM.TestCase): if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_policy_route.py b/smoketest/scripts/cli/test_policy_route.py index 53761b7d6..939e4bc53 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-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,7 +17,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from base_vyostest_shim import CSTORE_GUARD_TIME mark = '100' conn_mark = '555' @@ -37,8 +36,6 @@ 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]) @@ -73,6 +70,8 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): ] self.verify_rules(ip_rule_search, inverse=True) + # always forward to base class + super().tearDown() def test_pbr_group(self): self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'network', '172.16.99.0/24']) @@ -307,5 +306,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) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_protocols_babel.py b/smoketest/scripts/cli/test_protocols_babel.py index 3a9ee2d62..e8e9e6d21 100755 --- a/smoketest/scripts/cli/test_protocols_babel.py +++ b/smoketest/scripts/cli/test_protocols_babel.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,7 +17,6 @@ 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 @@ -39,8 +38,6 @@ class TestProtocolsBABEL(VyOSUnitTestSHIM.TestCase): 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 @@ -52,6 +49,8 @@ class TestProtocolsBABEL(VyOSUnitTestSHIM.TestCase): # check process health and continuity self.assertEqual(self.daemon_pid, process_named_running(babel_daemon)) + # always forward to base class + super().tearDown() def test_01_basic(self): diversity_factor = '64' @@ -65,7 +64,7 @@ class TestProtocolsBABEL(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig('router babel', endsection='^exit') + frrconfig = self.getFRRconfig('router babel', stop_section='^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) @@ -84,7 +83,7 @@ class TestProtocolsBABEL(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig('router babel', endsection='^exit', empty_retry=5) + frrconfig = self.getFRRconfig('router babel', stop_section='^exit') for protocol in ipv4_protos: self.assertIn(f' redistribute ipv4 {protocol}', frrconfig) for protocol in ipv6_protos: @@ -153,7 +152,7 @@ class TestProtocolsBABEL(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig('router babel', endsection='^exit') + frrconfig = self.getFRRconfig('router babel', stop_section='^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) @@ -201,11 +200,11 @@ class TestProtocolsBABEL(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig('router babel', endsection='^exit') + frrconfig = self.getFRRconfig('router babel', stop_section='^exit') for interface in self._interfaces: self.assertIn(f' network {interface}', frrconfig) - iface_config = self.getFRRconfig(f'interface {interface}', endsection='^exit') + iface_config = self.getFRRconfig(f'interface {interface}', stop_section='^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) @@ -219,4 +218,4 @@ class TestProtocolsBABEL(VyOSUnitTestSHIM.TestCase): self.assertIn(f' babel {type}', iface_config) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_protocols_bfd.py b/smoketest/scripts/cli/test_protocols_bfd.py index 2205cd9de..90235d632 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-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,7 +17,6 @@ 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 @@ -88,9 +87,6 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same 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 :) cls.cli_delete(cls, base_path) @@ -101,6 +97,8 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): # check process health and continuity self.assertEqual(self.daemon_pid, process_named_running(bfd_daemon)) + # always forward to base class + super().tearDown() def test_bfd_peer(self): self.cli_set(['vrf', 'name', vrf_name, 'table', '1000']) @@ -135,7 +133,7 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig('bfd', endsection='^exit') + frrconfig = self.getFRRconfig('bfd', stop_section='^exit') for peer, peer_config in peers.items(): tmp = f'peer {peer}' if 'multihop' in peer_config: @@ -148,8 +146,9 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): tmp += f' vrf {peer_config["vrf"]}' self.assertIn(tmp, frrconfig) - peerconfig = self.getFRRconfig('bfd', endsection='^exit', substring=f' peer {peer}', - endsubsection='^ exit') + peerconfig = self.getFRRconfig('bfd', stop_section='^exit', + start_subsection=f' peer {peer}', + stop_subsection='^ exit') if 'echo_mode' in peer_config: self.assertIn(f'echo-mode', peerconfig) if 'intv_echo' in peer_config: @@ -211,8 +210,8 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): # Verify FRR bgpd configuration for profile, profile_config in profiles.items(): - config = self.getFRRconfig('bfd', endsection='^exit', - substring=f' profile {profile}', endsubsection='^ exit',) + config = self.getFRRconfig('bfd', stop_section='^exit', + start_subsection=f' profile {profile}', stop_subsection='^ exit',) if 'echo_mode' in profile_config: self.assertIn(f' echo-mode', config) if 'intv_echo' in profile_config: @@ -234,10 +233,10 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): self.assertNotIn(f'shutdown', config) for peer, peer_config in peers.items(): - peerconfig = self.getFRRconfig('bfd', endsection='^exit', - substring=f' peer {peer}', endsubsection='^ exit') + peerconfig = self.getFRRconfig('bfd', stop_section='^exit', + start_subsection=f' peer {peer}', stop_subsection='^ exit') if 'profile' in peer_config: self.assertIn(f' profile {peer_config["profile"]}', peerconfig) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index 8403dcc37..c7c0dc6f0 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-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,7 +19,6 @@ 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 @@ -41,7 +40,7 @@ bfd_profile = 'foo-bar-baz' import_afi = 'ipv4-unicast' import_vrf = 'red' -import_rd = ASN + ':100' +import_rd = f'{ASN}:100' import_vrf_base = ['vrf', 'name'] neighbor_config = { '192.0.2.1' : { @@ -65,7 +64,7 @@ neighbor_config = { }, '192.0.2.2' : { 'bfd_profile' : bfd_profile, - 'remote_as' : '200', + 'remote_as' : 'auto', 'shutdown' : '', 'no_cap_nego' : '', 'port' : '667', @@ -112,7 +111,7 @@ neighbor_config = { 'local_role_strict': '', }, '2001:db8::2' : { - 'remote_as' : '456', + 'remote_as' : 'auto', 'shutdown' : '', 'no_cap_nego' : '', 'port' : '667', @@ -135,13 +134,14 @@ peer_group_config = { 'passive' : '', 'password' : 'VyOS-Secure123', 'shutdown' : '', + 'solo' : '', 'cap_over' : '', 'ttl_security' : '5', 'disable_conn_chk' : '', 'p_attr_discard' : ['100', '150', '200'], }, 'bar' : { - 'remote_as' : '111', + 'remote_as' : 'auto', 'graceful_rst_no' : '', 'port' : '667', 'p_attr_taw' : '126', @@ -201,17 +201,18 @@ 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']) cls.cli_delete(cls, ['policy', 'prefix-list']) cls.cli_delete(cls, ['policy', 'prefix-list6']) + super(TestProtocolsBGP, cls).tearDownClass() + def setUp(self): self.cli_set(base_path + ['system-as', ASN]) + # always forward to base class + super().setUp() def tearDown(self): # cleanup any possible VRF mess @@ -221,11 +222,13 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() - frrconfig = self.getFRRconfig('router bgp', endsection='^exit') + frrconfig = self.getFRRconfig('router bgp', stop_section='^exit') self.assertNotIn(f'router bgp', frrconfig) # check process health and continuity self.assertEqual(self.daemon_pid, process_named_running(bgp_daemon)) + # always forward to base class + super().tearDown() def create_bgp_instances_for_import_test(self): table = '1000' @@ -326,6 +329,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): tcp_keepalive_idle = '66' tcp_keepalive_interval = '77' tcp_keepalive_probes = '22' + max_delay = '120' + establish_wait = '60' self.cli_set(base_path + ['parameters', 'allow-martian-nexthop']) self.cli_set(base_path + ['parameters', 'disable-ebgp-connected-route-check']) @@ -358,6 +363,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['parameters', 'no-suppress-duplicates']) self.cli_set(base_path + ['parameters', 'reject-as-sets']) self.cli_set(base_path + ['parameters', 'route-reflector-allow-outbound-policy']) + self.cli_set(base_path + ['parameters', 'no-ipv6-auto-ra']) self.cli_set(base_path + ['parameters', 'shutdown']) self.cli_set(base_path + ['parameters', 'suppress-fib-pending']) self.cli_set(base_path + ['parameters', 'tcp-keepalive', 'idle', tcp_keepalive_idle]) @@ -372,11 +378,26 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['address-family', 'ipv6-unicast', 'maximum-paths', 'ebgp', max_path_v6]) self.cli_set(base_path + ['address-family', 'ipv6-unicast', 'maximum-paths', 'ibgp', max_path_v6ibgp]) + # BGP update-delay + self.cli_set( + base_path + ['parameters', 'update-delay', 'establish-wait', '200'] + ) + # establish-wait should not be set without max-delay + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + ['parameters', 'update-delay', 'max-delay', max_delay]) + # establish-wait should not be greater than max-delay + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set( + base_path + ['parameters', 'update-delay', 'establish-wait', establish_wait] + ) + # commit changes self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^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) @@ -394,32 +415,42 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' bgp bestpath compare-routerid', frrconfig) self.assertIn(f' bgp bestpath peer-type multipath-relax', frrconfig) self.assertIn(f' bgp minimum-holdtime {min_hold_time}', frrconfig) - self.assertIn(f' bgp reject-as-sets', frrconfig) + self.assertNotIn( + f'bgp reject-as-sets', frrconfig + ) # default was changed in FRR 10.5 to 'reject-as-sets' self.assertIn(f' bgp route-reflector allow-outbound-policy', frrconfig) + self.assertIn(f' no bgp ipv6-auto-ra', frrconfig) self.assertIn(f' bgp shutdown', frrconfig) self.assertIn(f' bgp suppress-fib-pending', frrconfig) self.assertIn(f' bgp tcp-keepalive {tcp_keepalive_idle} {tcp_keepalive_interval} {tcp_keepalive_probes}', frrconfig) self.assertNotIn(f'bgp ebgp-requires-policy', frrconfig) self.assertIn(f' no bgp suppress-duplicates', frrconfig) + self.assertIn(f' update-delay {max_delay} {establish_wait}', frrconfig) - afiv4_config = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', - substring=' address-family ipv4 unicast', - endsubsection='^ exit-address-family') + afiv4_config = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit', + start_subsection=' address-family ipv4 unicast', + stop_subsection='^ 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(f'router bgp {ASN}', endsection='^exit', - substring=' address-family ipv4 labeled-unicast', - endsubsection='^ exit-address-family') + afiv4_config = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit', + start_subsection=' address-family ipv4 labeled-unicast', + stop_subsection='^ 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(f'router bgp {ASN}', endsection='^exit', - substring=' address-family ipv6 unicast', - endsubsection='^ exit-address-family') + afiv6_config = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit', + start_subsection=' address-family ipv6 unicast', + stop_subsection='^ exit-address-family') self.assertIn(f' maximum-paths {max_path_v6}', afiv6_config) self.assertIn(f' maximum-paths ibgp {max_path_v6ibgp}', afiv6_config) + # Verify reject-as-sets + self.cli_delete(base_path + ['parameters', 'reject-as-sets']) + self.cli_commit() + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit') + self.assertIn(f'no bgp reject-as-sets', frrconfig) + def test_bgp_02_neighbors(self): # Test out individual neighbor configuration items, not all of them are # also available to a peer-group! @@ -523,7 +554,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) for peer, peer_config in neighbor_config.items(): @@ -570,6 +601,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['peer-group', peer_group, 'port', config["port"]]) if 'remote_as' in config: self.cli_set(base_path + ['peer-group', peer_group, 'remote-as', config["remote_as"]]) + if 'solo' in config: + self.cli_set(base_path + ['peer-group', peer_group, 'solo']) if 'shutdown' in config: self.cli_set(base_path + ['peer-group', peer_group, 'shutdown']) if 'ttl_security' in config: @@ -628,7 +661,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) for peer, peer_config in peer_group_config.items(): @@ -709,10 +742,10 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): 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']) + self.cli_set(proto_path + [table, 'metric'], value=table_config.get('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']) + self.cli_set(['policy', 'route-map', table_config.get('route_map'), 'rule', '10', 'action'], value='permit') + self.cli_set(proto_path + [table, 'route-map'], value=table_config.get('route_map')) else: self.cli_set(proto_path) if 'metric' in proto_config: @@ -738,7 +771,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(' address-family ipv4 unicast', frrconfig) @@ -841,10 +874,10 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): 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']) + self.cli_set(proto_path + [table, 'metric'], value=table_config.get('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']) + self.cli_set(['policy', 'route-map', table_config.get('route_map'), 'rule', '10', 'action'], value='permit') + self.cli_set(proto_path + [table, 'route-map'], value=table_config.get('route_map')) else: self.cli_set(proto_path) if 'metric' in proto_config: @@ -864,7 +897,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(' address-family ipv6 unicast', frrconfig) # T2100: By default ebgp-requires-policy is disabled to keep VyOS @@ -929,7 +962,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^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) @@ -964,7 +997,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' address-family l2vpn evpn', frrconfig) self.assertIn(f' advertise-all-vni', frrconfig) @@ -977,7 +1010,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}', endsection='^ exit-vni') + vniconfig = self.getFRRconfig(f' vni {vni}', stop_section='^ exit-vni') self.assertIn(f'vni {vni}', vniconfig) self.assertIn(f' advertise-default-gw', vniconfig) self.assertIn(f' advertise-svi-ip', vniconfig) @@ -1020,7 +1053,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR distances configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) for family in verify_families: self.assertIn(f'address-family {family}', frrconfig) @@ -1058,7 +1091,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' address-family ipv6 unicast', frrconfig) @@ -1066,7 +1099,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}', endsection='^exit') + frr_vrf_config = self.getFRRconfig(f'router bgp {ASN} vrf {vrf}', stop_section='^exit') self.assertIn(f'router bgp {ASN} vrf {vrf}', frr_vrf_config) self.assertIn(f' bgp router-id {router_id}', frr_vrf_config) @@ -1084,7 +1117,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^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) @@ -1101,7 +1134,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^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) @@ -1133,13 +1166,13 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) for afi in ['ipv4', 'ipv6']: - afi_config = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', - substring=f' address-family {afi} unicast', - endsubsection='^ exit-address-family') + afi_config = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit', + start_subsection=f' address-family {afi} unicast', + stop_subsection='^ 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) @@ -1184,7 +1217,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^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) @@ -1209,7 +1242,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^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) @@ -1234,8 +1267,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}', endsection='^exit') - frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit') + frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}', stop_section='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f'address-family ipv4 unicast', frrconfig) @@ -1257,8 +1290,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}', endsection='^exit') - frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit') + frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}', stop_section='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f'address-family ipv4 unicast', frrconfig) self.assertIn(f' import vrf {import_vrf}', frrconfig) @@ -1271,8 +1304,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}', endsection='^exit') - frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit') + frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}', stop_section='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f'router bgp {ASN} vrf {import_vrf}', frrconfig_vrf) self.cli_delete(base_path) @@ -1288,8 +1321,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): 'vpn', 'export', import_rd]) self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') - frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit') + frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}', stop_section='^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) @@ -1318,7 +1351,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() for interface in interfaces: - frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') + frrconfig = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(f'interface {interface}', frrconfig) self.assertIn(f' mpls bgp forwarding', frrconfig) @@ -1332,7 +1365,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() for interface in interfaces: - frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') + frrconfig = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(f'interface {interface}', frrconfig) self.assertIn(f' mpls bgp forwarding', frrconfig) self.cli_delete(['interfaces', 'ethernet', interface, 'vrf']) @@ -1352,7 +1385,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}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' segment-routing srv6', frrconfig) self.assertIn(f' locator {locator_name}', frrconfig) @@ -1367,20 +1400,20 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^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(f'router bgp {ASN}', endsection='^exit', - substring=' address-family ipv4 unicast', - endsubsection='^ exit-address-family') + afiv4_config = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit', + start_subsection=' address-family ipv4 unicast', + stop_subsection='^ 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(f'router bgp {ASN}', endsection='^exit', - substring=' address-family ipv6 unicast', - endsubsection='^ exit-address-family') + afiv6_config = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit', + start_subsection=' address-family ipv6 unicast', + stop_subsection='^ exit-address-family') self.assertIn(f' sid vpn export {sid}', afiv6_config) self.assertIn(f' nexthop vpn export {nexthop_ipv6}', afiv6_config) @@ -1396,16 +1429,16 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^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(f'router bgp {ASN}', endsection='^exit', - substring=' address-family ipv4 labeled-unicast', - endsubsection='^ exit-address-family') + afiv4_config = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit', + start_subsection=' address-family ipv4 labeled-unicast', + stop_subsection='^ 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) @@ -1422,31 +1455,78 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^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(f'router bgp {ASN}', endsection='^exit', - substring=' address-family ipv6 labeled-unicast', - endsubsection='^ exit-address-family') + afiv6_config = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit', + start_subsection=' address-family ipv6 labeled-unicast', + stop_subsection='^ 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) def test_bgp_27_route_reflector_client(self): - self.cli_set(base_path + ['peer-group', 'peer1', 'address-family', 'l2vpn-evpn', 'route-reflector-client']) - with self.assertRaises(ConfigSessionError) as e: - self.cli_commit() - - self.cli_set(base_path + ['peer-group', 'peer1', 'remote-as', 'internal']) + int_neighbors = ['192.0.2.2', '192.0.2.3', '192.0.2.4', '192.0.2.5'] + int_interfaces = ['dum0', 'dum1', 'dum2', 'dum3'] + int_pg_names = ['SMOKETESTINT0', 'SMOKETESTINT1', 'SMOKETESTINT2'] + remote_as_types = ['external', 'internal'] + for int_interface in int_interfaces: + self.cli_set(['interfaces', 'dummy', int_interface]) self.cli_commit() - conf = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', - substring=' address-family l2vpn evpn', endsubsection='^ exit-address-family') + def _set_neighbor_0(neighbor, remote_as_type): + # set route-reflector-client in neighbor and set remote-as in peer_group + interface_cmd = ['interface'] if neighbor.startswith('dum') else [] + self.cli_set(base_path + ['peer-group', int_pg_names[0], 'remote-as', remote_as_type]) + self.cli_set(base_path + ['neighbor', neighbor, 'address-family', 'ipv4-unicast', 'route-reflector-client']) + self.cli_set(base_path + ['neighbor', neighbor] + interface_cmd + ['peer-group', int_pg_names[0]]) + + def _set_neighbor_1(neighbor, remote_as_type): + # set route-reflector-client in peer_group and set remote-as in neighbor + interface_cmd = ['interface'] if neighbor.startswith('dum') else [] + self.cli_set(base_path + ['peer-group', int_pg_names[1], 'address-family', 'ipv4-unicast', 'route-reflector-client']) + self.cli_set(base_path + ['neighbor', neighbor] + interface_cmd + ['remote-as', remote_as_type]) + self.cli_set(base_path + ['neighbor', neighbor] + interface_cmd + ['peer-group', int_pg_names[1]]) + + def _set_neighbor_2(neighbor, remote_as_type): + # set route-reflector-client and remote-as in peer_group + interface_cmd = ['interface'] if neighbor.startswith('dum') else [] + self.cli_set(base_path + ['peer-group', int_pg_names[2], 'remote-as', remote_as_type]) + self.cli_set(base_path + ['peer-group', int_pg_names[2], 'address-family', 'ipv4-unicast', 'route-reflector-client']) + self.cli_set(base_path + ['neighbor', neighbor] + interface_cmd + ['peer-group', int_pg_names[2]]) + + def _set_neighbor_3(neighbor, remote_as_type): + # set route-reflector-client and remote-as in neighbor + interface_cmd = ['interface'] if neighbor.startswith('dum') else [] + self.cli_set(base_path + ['neighbor', neighbor, 'address-family', 'ipv4-unicast', 'route-reflector-client']) + self.cli_set(base_path + ['neighbor', neighbor] + interface_cmd + ['remote-as', remote_as_type]) + + set_neighbor_funcs = [_set_neighbor_0, _set_neighbor_1, _set_neighbor_2, _set_neighbor_3] + for remote_as_type in remote_as_types: + for func_count, set_neighbor_func in enumerate(set_neighbor_funcs): + for neighbors in [int_neighbors, int_interfaces]: + set_neighbor_func(neighbors[func_count], remote_as_type) + if remote_as_type == 'external': + with self.assertRaises(ConfigSessionError) as e: + self.cli_commit() + self.cli_discard() + else: + self.cli_commit() + + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit', start_subsection=' address-family ipv4 unicast', stop_subsection='^ exit-address-family') + neighbor_has_rr_client = [ + int_neighbors[0], int_neighbors[3], + int_interfaces[0], int_interfaces[3], + int_pg_names[1], int_pg_names[2], + ] + [self.assertIn(f'neighbor {neighbor} route-reflector-client', frrconfig) for neighbor in neighbor_has_rr_client] - self.assertIn('neighbor peer1 route-reflector-client', conf) + # tearDown dummy interfaces + self.cli_delete(['interfaces', 'dummy']) + self.cli_commit() def test_bgp_28_peer_group_member_all_internal_or_external(self): def _common_config_check(conf, include_ras=True): @@ -1483,7 +1563,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}', endsection='^exit') + conf = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit') _common_config_check(conf) # test add internal remote-as to external group @@ -1498,7 +1578,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}', endsection='^exit') + conf = self.getFRRconfig(f'router bgp {ASN}', stop_section='^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) @@ -1510,7 +1590,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}', endsection='^exit') + conf = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit') _common_config_check(conf, include_ras=False) self.assertIn(f'neighbor {int_neighbors[0]} remote-as internal', conf) @@ -1535,7 +1615,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() - conf = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + conf = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit') self.assertIn(f'neighbor OVERLAY remote-as {int(ASN) + 1}', conf) self.assertIn(f'neighbor OVERLAY local-as {int(ASN) + 1}', conf) @@ -1562,7 +1642,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): # Verify FRR bgpd configuration frrconfig = self.getFRRconfig(f'router bgp {ASN}', - endsection='^exit') + stop_section='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' address-family ipv4 unicast', frrconfig) @@ -1571,11 +1651,75 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): # Verify FRR bgpd configuration frr_vrf_config = self.getFRRconfig( - f'router bgp {ASN} vrf {vrf}', endsection='^exit') + f'router bgp {ASN} vrf {vrf}', stop_section='^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_31_as_notation(self): + # Test BGP AS-notation output format for both global and VRF BGP instances. + # Verify all three notation types render correctly on the router bgp line. + router_id = '127.0.0.4' + vrf = 'red' + + for vrf in ['', 'red']: + vrf_base = ['vrf', 'name', vrf] if vrf else [] + vrf_frr_bit = f' vrf {vrf}' if vrf else '' + bgp_path = vrf_base + base_path + + if vrf_base: + self.cli_set(vrf_base + ['table', '2000']) + + self.cli_set(bgp_path + ['system-as', ASN]) + self.cli_set(bgp_path + ['parameters', 'router-id', router_id]) + + for notation in ['asdot', 'asdot+']: + self.cli_set(bgp_path + ['parameters', 'as-notation', notation]) + self.cli_commit() + + # our config options are called 'asdot' and 'asdot+' as in + # RFC 5396 + # but FRR calls them 'dot' and 'dot+' in the router bgp line + # e.g. router bgp 12345 as-notation dot + # or + # router bgp 12345 vrf red as-notation dot + router_str = f'router bgp {ASN}{vrf_frr_bit} as-notation {notation.replace("as", "")}' + # getFRRConfig interprets the arguments as regex, so escape '+' if present + frrconfig = self.getFRRconfig( + router_str.replace('+', r'\+'), stop_section='^exit' + ) + self.assertIn(router_str, frrconfig) + + # Verify removing as-notation works + self.cli_delete(bgp_path + ['parameters', 'as-notation']) + self.cli_commit() + + router_str = f'router bgp {ASN}{vrf_frr_bit}' + + frrconfig = self.getFRRconfig(router_str, stop_section='^exit') + self.assertIn(router_str, frrconfig) + self.assertNotIn('as-notation', frrconfig.splitlines()[0]) + + # Cleanup + self.cli_delete(bgp_path) + if vrf_base: + self.cli_delete(vrf_base) + + self.cli_commit() + + def test_bgp_32_bfd_strict(self): + neighbor = '192.0.2.22' + bfd_hold_time = '23' + + self.cli_set(base_path + ['neighbor', neighbor, 'remote-as', ASN]) + self.cli_set(base_path + ['neighbor', neighbor, 'bfd', 'strict', 'hold-time', bfd_hold_time]) + + self.cli_commit() + + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit') + self.assertIn(f'router bgp {ASN}', frrconfig) + self.assertIn(f' neighbor {neighbor} bfd strict hold-time {bfd_hold_time}', frrconfig) + def test_bgp_99_bmp(self): target_name = 'instance-bmp' target_address = '127.0.0.1' @@ -1620,17 +1764,37 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_set(target_path + ['max-retry', max_retry]) self.cli_set(target_path + ['mirror']) self.cli_set(target_path + ['monitor', 'ipv4-unicast', monitor_ipv4]) + self.cli_set(target_path + ['monitor', 'ipv4-unicast', 'local-rib']) self.cli_set(target_path + ['monitor', 'ipv6-unicast', monitor_ipv6]) + self.cli_set(target_path + ['monitor', 'ipv6-unicast', 'local-rib']) self.cli_commit() # Verify bgpd bmp configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^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) self.assertIn(f'bmp monitor ipv4 unicast {monitor_ipv4}', frrconfig) self.assertIn(f'bmp monitor ipv6 unicast {monitor_ipv6}', frrconfig) + self.assertIn(f'bmp monitor ipv4 unicast loc-rib', frrconfig) + self.assertIn(f'bmp monitor ipv6 unicast loc-rib', frrconfig) self.assertIn(f'bmp connect {target_address} port {target_port} min-retry {min_retry} max-retry {max_retry}', frrconfig) + def test_bgp_100_link_state(self): + router_id = '127.0.0.1' + peer = '192.0.3.3' + peer_asn = '100' + + self.cli_set(base_path + ['parameters', 'router-id', router_id]) + self.cli_set(base_path + ['neighbor', peer, 'remote-as', peer_asn]) + + self.cli_set(base_path + ['neighbor', peer, 'address-family', 'link-state']) + + self.cli_commit() + + # Verify FRR bgpd configuration + frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit') + self.assertIn(f' address-family link-state', frrconfig) + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_protocols_failover.py b/smoketest/scripts/cli/test_protocols_failover.py new file mode 100755 index 000000000..afdaac7c2 --- /dev/null +++ b/smoketest/scripts/cli/test_protocols_failover.py @@ -0,0 +1,649 @@ +#!/usr/bin/env python3 +# +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> +# +# 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 shutil +import unittest + +from math import ceil +from time import sleep + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.utils.process import ip_cmd + +vrf_base_path = ['vrf', 'name'] +red = 'red-309aba83' +blue = 'blue-46b27cb' +used_vrf_names = [red, blue] +base_path = ['protocols', 'failover'] + +config_dir_root = '/run/vyos-failover.conf.d' + +check_timeout = 1 +wait_timeout = 5 +wait_dhcp_timeout = 10 + +# Use numeric value to not get ip errors while +# /etc/iproute2/rt_protos.d/failover.conf is not installed yet +failover_protocol_value = 111 + +dummy_if1 = 'dum3711' +dummy_if2 = 'dum3712' +dummy_if3 = 'dum3713' + +veth_if1 = 'veth71' +veth_if2 = 'veth72' + +route_prefix = '203.0.113.0/24' +route2_prefix = '172.16.0.0/24' +route_base_path = base_path + ['route', route_prefix] + +dummy_if1_addr = '192.168.30.1' +dummy_if2_addr = '10.0.70.1' +dummy_if3_addr = '10.20.0.1' + +# These three must be in same subnet: +dhcp_prefix = '10.133.0' +veth_if1_addr = f'{dhcp_prefix}.1' +dhcp_gateway_addr_1 = f'{dhcp_prefix}.99' +dhcp_gateway_addr_2 = f'{dhcp_prefix}.117' + + +class RoutesChecker: + def __init__(self, required_routes, allow_extra=False): + self.required_routes = required_routes + self.allow_extra = allow_extra + self.error = '' + + def __call__(self, got_routes): + self.error = '' + if len(got_routes) < len(self.required_routes): + self.error = f"Not enough routes: expected {len(self.required_routes)}, got {len(got_routes)}: {got_routes}" + return False + + if not self.allow_extra and len(got_routes) != len(self.required_routes): + self.error = f"Extra routes: expected {len(self.required_routes)}, got {len(got_routes)}: {got_routes}" + + for route in self.required_routes: + found = False + for got_route in got_routes: + mismatch = False + for key in route: + if route[key] is None: + if key in got_route: + mismatch = True + break + elif key not in got_route or got_route[key] != route[key]: + mismatch = True + break + if not mismatch: + found = True + break + if not found: + self.error = f"Couldn't find required route {route} among received routes {got_routes}" + return False + return True + + +class TestProtocolsFailover(VyOSUnitTestSHIM.TestCase): + def clean_and_stop_daemon(self): + self.cli_delete(base_path) + for vrf in used_vrf_names: + self.cli_delete(vrf_base_path + [vrf] + base_path) + self.cli_commit() + shutil.rmtree(config_dir_root, ignore_errors=True) + + def setUp(self): + # Needed dummy interfaces + self.cli_set(['interfaces', 'dummy', dummy_if1]) + self.cli_set(['interfaces', 'dummy', dummy_if2]) + self.cli_set(['interfaces', 'dummy', dummy_if3]) + self.cli_set( + ['interfaces', 'virtual-ethernet', veth_if1, 'peer-name', veth_if2] + ) + self.cli_set( + ['interfaces', 'virtual-ethernet', veth_if2, 'peer-name', veth_if1] + ) + + self.clean_and_stop_daemon() + # always forward to base class + super().setUp() + + self.clean_dhclient_lease_files = set() + self.need_dhcp_dir_cleanup = False + + def tearDown(self): + self.cli_delete(['interfaces', 'virtual-ethernet', veth_if2]) + self.cli_delete(['interfaces', 'virtual-ethernet', veth_if1]) + self.cli_delete(['interfaces', 'dummy', dummy_if3]) + self.cli_delete(['interfaces', 'dummy', dummy_if2]) + self.cli_delete(['interfaces', 'dummy', dummy_if1]) + self.cli_delete(['service', 'dhcp-server']) + self.cli_delete(['service', 'dns']) + + self.clean_and_stop_daemon() + + failover_routes = ip_cmd( + f'route show proto {failover_protocol_value} table all' + ) + self.assertEqual(failover_routes, [], "Some failover IPv4 routes left") + failover_routes = ip_cmd( + f'-6 route show proto {failover_protocol_value} table all' + ) + self.assertEqual(failover_routes, [], "Some failover IPv6 routes left") + # always forward to base class + super().tearDown() + + def wait_for_ip_output(self, ip_command_args, check, pause=0.1, timeout=3): + tries = ceil(timeout / pause) + result = None + for i in range(tries): + result = ip_cmd(ip_command_args) + if callable(check): + if check(result): + return True, result + elif result == check: + return True, result + + sleep(pause) + + return False, result + + def test_01_basic(self): + res, output = self.wait_for_ip_output( + f'route show proto {failover_protocol_value}', + [], + timeout=wait_timeout, + ) + self.assertTrue( + res, f"No failover routes must exist before test, last result: {output}" + ) + + self.cli_set( + ['interfaces', 'dummy', dummy_if1, 'address', dummy_if1_addr + '/24'] + ) + self.cli_set( + ['interfaces', 'dummy', dummy_if2, 'address', dummy_if2_addr + '/24'] + ) + self.cli_set( + route_base_path + ['next-hop', dummy_if2_addr, 'interface', dummy_if2] + ) + self.cli_set(route_base_path + ['next-hop', dummy_if2_addr, 'metric', '30']) + self.cli_set( + route_base_path + + [ + 'next-hop', + dummy_if2_addr, + 'check', + 'target', + dummy_if1_addr, + 'interface', + dummy_if1, + ] + ) + self.cli_set( + route_base_path + + ['next-hop', dummy_if2_addr, 'check', 'timeout', str(check_timeout)] + ) + self.cli_commit() + + # Now vyos-failover must be launched, it should create route, waiting for it... + checker = RoutesChecker([{'dst': route_prefix}]) + res, output = self.wait_for_ip_output( + f"route show proto {failover_protocol_value}", + checker, + timeout=wait_timeout, + ) + self.assertTrue(res, f"Route must have been created, last result: {output}") + + self.cli_delete(['interfaces', 'dummy', dummy_if1, 'address']) + self.cli_commit() + + res, output = self.wait_for_ip_output( + f'route show proto {failover_protocol_value}', + [], + timeout=wait_timeout, + ) + self.assertTrue(res, f"Route must have been deleted, last result: {output}") + + def test_02_vrf(self): + # route 1 default VRF, check red + # route 2 red, check blue + # route 3 red, check red + # route 1 and route 3 with same destination + + res, output = self.wait_for_ip_output( + f'route show proto {failover_protocol_value} table all', + [], + timeout=wait_timeout, + ) + self.assertTrue( + res, f"No failover routes must exist before test, last result: {output}" + ) + + self.cli_set(['vrf', 'name', red, 'table', '43310']) + self.cli_set(['vrf', 'name', blue, 'table', '43311']) + + self.cli_set( + ['interfaces', 'dummy', dummy_if1, 'address', dummy_if1_addr + '/24'] + ) + self.cli_set( + ['interfaces', 'dummy', dummy_if2, 'address', dummy_if2_addr + '/24'] + ) + self.cli_set( + ['interfaces', 'dummy', dummy_if3, 'address', dummy_if3_addr + '/24'] + ) + self.cli_set(['interfaces', 'dummy', dummy_if2, 'vrf', red]) + self.cli_set(['interfaces', 'dummy', dummy_if3, 'vrf', blue]) + + route_1_base = route_base_path + route_2_base = vrf_base_path + [red] + base_path + ['route', route2_prefix] + route_3_base = vrf_base_path + [red] + base_path + ['route', route_prefix] + + self.cli_set( + route_1_base + ['next-hop', dummy_if1_addr, 'interface', dummy_if1] + ) + self.cli_set( + route_1_base + + [ + 'next-hop', + dummy_if1_addr, + 'check', + 'target', + dummy_if2_addr, + 'vrf', + red, + ] + ) + self.cli_set( + route_1_base + + ['next-hop', dummy_if1_addr, 'check', 'timeout', str(check_timeout)] + ) + + self.cli_set( + route_2_base + ['next-hop', dummy_if2_addr, 'interface', dummy_if2] + ) + self.cli_set( + route_2_base + + [ + 'next-hop', + dummy_if2_addr, + 'check', + 'target', + dummy_if3_addr, + 'vrf', + blue, + ] + ) + self.cli_set( + route_2_base + + ['next-hop', dummy_if2_addr, 'check', 'timeout', str(check_timeout)] + ) + + self.cli_set( + route_3_base + ['next-hop', dummy_if2_addr, 'interface', dummy_if2] + ) + self.cli_set( + route_3_base + + [ + 'next-hop', + dummy_if2_addr, + 'check', + 'target', + dummy_if2_addr, + ] + ) + self.cli_set( + route_3_base + + ['next-hop', dummy_if2_addr, 'check', 'timeout', str(check_timeout)] + ) + + self.cli_commit() + + route1_fields = {'dst': route_prefix, 'gateway': dummy_if1_addr, 'table': None} + route2_fields = { + 'dst': route2_prefix, + 'gateway': dummy_if2_addr, + 'table': red, + } + route3_fields = {'dst': route_prefix, 'gateway': dummy_if2_addr, 'table': red} + + # All three routes must be created + checker1 = RoutesChecker([route1_fields, route2_fields, route3_fields]) + res, output = self.wait_for_ip_output( + f"route show proto {failover_protocol_value} table all", + checker1, + timeout=wait_timeout * 3, + ) + self.assertTrue( + res, f"Routes must have been created, checker error: {checker1.error}" + ) + + # Delete dummy_if3, route2 must be deleted + self.cli_delete(['interfaces', 'dummy', dummy_if3, 'address']) + self.cli_commit() + + checker2 = RoutesChecker([route1_fields, route3_fields]) + res, output = self.wait_for_ip_output( + f'route show proto {failover_protocol_value} table all', + checker2, + timeout=wait_timeout * 3, + ) + self.assertTrue( + res, + f"Only route1 and route3 must have been left, checker error: {checker2.error}", + ) + + # Delete dummy_if2, all routes must be deleted + self.cli_delete(['interfaces', 'dummy', dummy_if2, 'address']) + self.cli_commit() + res, output = self.wait_for_ip_output( + f'route show proto {failover_protocol_value} table all', + [], + timeout=wait_timeout * 3, + ) + self.assertTrue(res, f"No routes should have been left, got: {output}") + + def test_03_config(self): + # Test how daemon reacts to routes add/delete, files add/delete + # All checks in this test are always true, configuration is added/deleted only + + # route 1 default VRF + # route 2 default VRF + # route 3 red + route1_fields = {'dst': route_prefix, 'gateway': dummy_if1_addr, 'table': None} + route2_fields = {'dst': route2_prefix, 'gateway': dummy_if1_addr, 'table': None} + route3_fields = {'dst': route_prefix, 'gateway': dummy_if2_addr, 'table': red} + + res, output = self.wait_for_ip_output( + f'route show proto {failover_protocol_value} table all', + [], + timeout=wait_timeout, + ) + self.assertTrue( + res, f"No failover routes must exist before test, last result: {output}" + ) + + self.cli_set(['vrf', 'name', red, 'table', '43310']) + self.cli_set(['vrf', 'name', blue, 'table', '43311']) + + self.cli_set( + ['interfaces', 'dummy', dummy_if1, 'address', dummy_if1_addr + '/24'] + ) + self.cli_set( + ['interfaces', 'dummy', dummy_if2, 'address', dummy_if2_addr + '/24'] + ) + self.cli_set(['interfaces', 'dummy', dummy_if2, 'vrf', red]) + + route_1_base = route_base_path + route_2_base = base_path + ['route', route2_prefix] + route_3_base = vrf_base_path + [red] + base_path + ['route', route_prefix] + + self.cli_set( + route_1_base + ['next-hop', dummy_if1_addr, 'interface', dummy_if1] + ) + self.cli_set( + route_1_base + + [ + 'next-hop', + dummy_if1_addr, + 'check', + 'target', + dummy_if1_addr, + ] + ) + self.cli_set( + route_1_base + + ['next-hop', dummy_if1_addr, 'check', 'timeout', str(check_timeout)] + ) + self.cli_commit() + + # Adding only route1 + checker1 = RoutesChecker([route1_fields]) + res, output = self.wait_for_ip_output( + f"route show proto {failover_protocol_value} table all", + checker1, + timeout=wait_timeout * 3, + ) + self.assertTrue( + res, f"Route 1 must have been created, checker error: {checker1.error}" + ) + + # adding route 3 - new file + self.cli_set( + route_3_base + ['next-hop', dummy_if2_addr, 'interface', dummy_if2] + ) + self.cli_set( + route_3_base + + [ + 'next-hop', + dummy_if2_addr, + 'check', + 'target', + dummy_if2_addr, + ] + ) + self.cli_set( + route_3_base + + ['next-hop', dummy_if2_addr, 'check', 'timeout', str(check_timeout)] + ) + self.cli_commit() + + # Now route1 and route3 must be active + checker13 = RoutesChecker([route1_fields, route3_fields]) + res, output = self.wait_for_ip_output( + f"route show proto {failover_protocol_value} table all", + checker13, + timeout=wait_timeout * 3, + ) + self.assertTrue( + res, + f"Route 1 and route 3 must have been created, checker error: {checker13.error}", + ) + + # Now add route2 (add of route to file) + self.cli_set( + route_2_base + ['next-hop', dummy_if1_addr, 'interface', dummy_if1] + ) + self.cli_set( + route_2_base + + [ + 'next-hop', + dummy_if1_addr, + 'check', + 'target', + dummy_if1_addr, + ] + ) + self.cli_set( + route_2_base + + ['next-hop', dummy_if1_addr, 'check', 'timeout', str(check_timeout)] + ) + self.cli_commit() + + # All three routes must be created + checker123 = RoutesChecker([route1_fields, route2_fields, route3_fields]) + res, output = self.wait_for_ip_output( + f'route show proto {failover_protocol_value} table all', + checker123, + timeout=wait_timeout * 3, + ) + self.assertTrue( + res, + f"All three routes must have been created, checker error: {checker123.error}", + ) + + # Delete route1 + self.cli_delete(route_1_base) + self.cli_commit() + + # Now route2 and route3 must be active + checker23 = RoutesChecker([route2_fields, route3_fields]) + res, output = self.wait_for_ip_output( + f"route show proto {failover_protocol_value} table all", + checker23, + timeout=wait_timeout * 3, + ) + self.assertTrue( + res, + f"Route 1 must have been deleted, routes 2 and 3 active. Checker error: {checker23.error}", + ) + + # Delete route2 - file deletion + self.cli_delete(route_2_base) + self.cli_commit() + + # Now only route3 must be active + checker3 = RoutesChecker([route3_fields]) + res, output = self.wait_for_ip_output( + f"route show proto {failover_protocol_value} table all", + checker3, + timeout=wait_timeout * 3, + ) + self.assertTrue( + res, + f"Route 2 must have been deleted, only routes 3 should be active. Checker error: {checker3.error}", + ) + + # Deleting last route + self.cli_delete(route_3_base) + self.cli_commit() + + res, output = self.wait_for_ip_output( + f'route show proto {failover_protocol_value} table all', + [], + timeout=wait_timeout * 3, + ) + self.assertTrue(res, f"No routes should have been left, got: {output}") + + + def test_04_dhcp(self): + res, output = self.wait_for_ip_output( + f'route show proto {failover_protocol_value}', + [], + timeout=wait_timeout, + ) + self.assertTrue( + res, f"No failover routes must exist before test, last result: {output}" + ) + + # Setup DHCP server + self.cli_set( + [ + 'interfaces', + 'virtual-ethernet', + veth_if1, + 'address', + f'{dhcp_prefix}.1/24', + ] + ) + self.cli_set(['interfaces', 'virtual-ethernet', veth_if1, 'description', 'LAN']) + + service_base = [ + 'service', + 'dhcp-server', + 'shared-network-name', + 'LAN', + 'subnet', + f'{dhcp_prefix}.0/24', + ] + self.cli_set(service_base + ['option', 'name-server', f'{dhcp_prefix}.1']) + self.cli_set(service_base + ['option', 'domain-name', 'vyos']) + self.cli_set(service_base + ['lease', '86400']) + self.cli_set(service_base + ['range', '0', 'start', f'{dhcp_prefix}.9']) + self.cli_set(service_base + ['range', '0', 'stop', f'{dhcp_prefix}.254']) + self.cli_set(service_base + ['subnet-id', '1952']) + + self.cli_set(['service', 'dns', 'forwarding', 'cache-size', '0']) + self.cli_set( + ['service', 'dns', 'forwarding', 'listen-address', f'{dhcp_prefix}.1'] + ) + self.cli_set( + ['service', 'dns', 'forwarding', 'allow-from', f'{dhcp_prefix}.0/24'] + ) + # End setup DHCP server + + # Setting first DHCP Gateway address + self.cli_set(service_base + ['option', 'default-router', dhcp_gateway_addr_1]) + + self.cli_set( + ['interfaces', 'dummy', dummy_if1, 'address', dummy_if1_addr + '/24'] + ) + self.cli_set( + ['interfaces', 'dummy', dummy_if2, 'address', dummy_if2_addr + '/24'] + ) + self.cli_set(['interfaces', 'virtual-ethernet', veth_if2, 'address', 'dhcp']) + self.cli_set(route_base_path + ['dhcp-interface', veth_if2]) + base_dhcp_interface = route_base_path + ['dhcp-interface', veth_if2] + self.cli_set(base_dhcp_interface + ['metric', '30']) + self.cli_set( + base_dhcp_interface + + [ + 'check', + 'target', + dummy_if1_addr, + 'interface', + dummy_if1, + ] + ) + self.cli_set(base_dhcp_interface + ['check', 'timeout', str(check_timeout)]) + self.cli_commit() + + # Now vyos-failover must be launched, it should create route to first dhcp address + checker = RoutesChecker([{'dst': route_prefix, 'gateway': dhcp_gateway_addr_1}]) + res, output = self.wait_for_ip_output( + f"route show proto {failover_protocol_value}", + checker, + timeout=wait_dhcp_timeout, + ) + self.assertTrue( + res, + f"Route must have been created via fist dhcp address. Checker error: {checker.error}", + ) + + # Change DHCP gateway address + renew_cmd = ['renew', 'dhcp', 'interface', veth_if2] + self.cli_set(service_base + ['option', 'default-router', dhcp_gateway_addr_2]) + self.cli_commit() + self.op_mode(renew_cmd) + + checker = RoutesChecker([{'dst': route_prefix, 'gateway': dhcp_gateway_addr_2}]) + res, output = self.wait_for_ip_output( + f"route show proto {failover_protocol_value}", + checker, + timeout=wait_dhcp_timeout, + ) + self.assertTrue( + res, + f"Route must have been created via second dhcp address. Checker error: {checker.error}", + ) + + # DHCP server down + self.cli_delete(['service', 'dhcp-server']) + self.cli_delete(['service', 'dns']) + self.cli_commit() + self.op_mode(renew_cmd) + + res, output = self.wait_for_ip_output( + f'route show proto {failover_protocol_value}', + [], + timeout=wait_dhcp_timeout, + ) + self.assertTrue(res, f"Route must have been deleted, last result: {output}") + + +if __name__ == '__main__': + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_protocols_igmp-proxy.py b/smoketest/scripts/cli/test_protocols_igmp-proxy.py index df10442ea..64244ec38 100755 --- a/smoketest/scripts/cli/test_protocols_igmp-proxy.py +++ b/smoketest/scripts/cli/test_protocols_igmp-proxy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,11 +19,12 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError +from vyos.defaults import config_files from vyos.utils.file import read_file from vyos.utils.process import process_named_running PROCESS_NAME = 'igmpproxy' -IGMP_PROXY_CONF = '/etc/igmpproxy.conf' +IGMP_PROXY_CONF = config_files['igmp_proxy'] base_path = ['protocols', 'igmp-proxy'] upstream_if = 'eth1' downstream_if = 'eth2' @@ -54,6 +55,8 @@ class TestProtocolsIGMPProxy(VyOSUnitTestSHIM.TestCase): # Check for no longer running process self.assertFalse(process_named_running(PROCESS_NAME)) + # always forward to base class + super().tearDown() def test_igmpproxy(self): threshold = '20' @@ -93,4 +96,4 @@ class TestProtocolsIGMPProxy(VyOSUnitTestSHIM.TestCase): self.assertIn(f'phyint {downstream_if} downstream ratelimit 0 threshold 1', config) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_protocols_isis.py b/smoketest/scripts/cli/test_protocols_isis.py index 14e833fd9..c0d505616 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-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,7 +17,6 @@ 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 @@ -40,8 +39,6 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): # 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 @@ -53,6 +50,8 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): # check process health and continuity self.assertEqual(self.daemon_pid, process_named_running(isis_daemon)) + # always forward to base class + super().tearDown() def test_isis_01_redistribute(self): prefix_list = 'EXPORT-ISIS' @@ -90,7 +89,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') + tmp = self.getFRRconfig(f'router isis {domain}', stop_section='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' metric-style {metric_style}', tmp) self.assertIn(f' log-adjacency-changes', tmp) @@ -98,7 +97,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.assertIn(f' redistribute ipv4 {proto} level-2 route-map {route_map}', tmp) for interface in self._interfaces: - tmp = self.getFRRconfig(f'interface {interface}', endsection='^exit') + tmp = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(f' ip router isis {domain}', tmp) self.assertIn(f' ipv6 router isis {domain}', tmp) @@ -127,11 +126,11 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR isisd configuration - tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') + tmp = self.getFRRconfig(f'router isis {domain}', stop_section='^exit') self.assertIn(f'router isis {domain}', tmp) self.assertIn(f' net {net}', tmp) - tmp = self.getFRRconfig(f'router isis {domain} vrf {vrf}', endsection='^exit') + tmp = self.getFRRconfig(f'router isis {domain} vrf {vrf}', stop_section='^exit') self.assertIn(f'router isis {domain} vrf {vrf}', tmp) self.assertIn(f' net {net}', tmp) self.assertIn(f' advertise-high-metrics', tmp) @@ -158,7 +157,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') + tmp = self.getFRRconfig(f'router isis {domain}', stop_section='^exit') self.assertIn(f' net {net}', tmp) for afi in ['ipv4', 'ipv6']: @@ -168,6 +167,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): def test_isis_05_password(self): password = 'foo' + md5_password = 'secret_md5_hash' self.cli_set(base_path + ['net', net]) for interface in self._interfaces: @@ -192,15 +192,38 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - tmp = self.getFRRconfig(f'router isis {domain}', endsection='exit') + tmp = self.getFRRconfig(f'router isis {domain}', stop_section='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}', endsection='^exit') + tmp = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(f' isis password clear {password}-{interface}', tmp) + # Switch to MD5 passwords - delete plaintext passwords first + self.cli_delete(base_path + ['area-password', 'plaintext-password']) + self.cli_delete(base_path + ['domain-password', 'plaintext-password']) + for interface in self._interfaces: + self.cli_delete(base_path + ['interface', interface, 'password', 'plaintext-password']) + + self.cli_set(base_path + ['domain-password', 'md5', md5_password]) + self.cli_set(base_path + ['area-password', 'md5', md5_password]) + for interface in self._interfaces: + self.cli_set(base_path + ['interface', interface, 'password', 'md5', md5_password]) + + # Commit all changes + self.cli_commit() + + # Verify all changes + tmp = self.getFRRconfig(f'router isis {domain}', stop_section='exit') + self.assertIn(f' domain-password md5 {md5_password}', tmp) + self.assertIn(f' area-password md5 {md5_password}', tmp) + + for interface in self._interfaces: + tmp = self.getFRRconfig(f'interface {interface}', stop_section='^exit') + self.assertIn(f' isis password md5 {md5_password}', tmp) + def test_isis_06_spf_delay_bfd(self): network = 'point-to-point' holddown = '10' @@ -240,12 +263,12 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') + tmp = self.getFRRconfig(f'router isis {domain}', stop_section='^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}', endsection='^exit') + tmp = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(f' ip router isis {domain}', tmp) self.assertIn(f' ipv6 router isis {domain}', tmp) self.assertIn(f' isis network {network}', tmp) @@ -289,7 +312,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') + tmp = self.getFRRconfig(f'router isis {domain}', stop_section='^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) @@ -311,7 +334,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify main ISIS changes - tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') + tmp = self.getFRRconfig(f'router isis {domain}', stop_section='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' mpls ldp-sync', tmp) self.assertIn(f' mpls ldp-sync holddown {holddown}', tmp) @@ -324,7 +347,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): for interface in self._interfaces: # Verify interface changes for holddown - tmp = self.getFRRconfig(f'interface {interface}', endsection='^exit') + tmp = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(f'interface {interface}', tmp) self.assertIn(f' ip router isis {domain}', tmp) self.assertIn(f' ipv6 router isis {domain}', tmp) @@ -338,7 +361,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): for interface in self._interfaces: # Verify interface changes for disable - tmp = self.getFRRconfig(f'interface {interface}', endsection='^exit') + tmp = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(f'interface {interface}', tmp) self.assertIn(f' ip router isis {domain}', tmp) self.assertIn(f' ipv6 router isis {domain}', tmp) @@ -361,7 +384,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}', endsection='^exit') + tmp = self.getFRRconfig(f'router isis {domain}', stop_section='^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']) @@ -371,7 +394,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}', endsection='^exit') + tmp = self.getFRRconfig(f'router isis {domain}', stop_section='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' fast-reroute load-sharing disable {level}', tmp) self.cli_delete(base_path + ['fast-reroute']) @@ -382,7 +405,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}', endsection='^exit') + tmp = self.getFRRconfig(f'router isis {domain}', stop_section='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' fast-reroute priority-limit {priority} {level}', tmp) self.cli_delete(base_path + ['fast-reroute']) @@ -394,7 +417,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}', endsection='^exit') + tmp = self.getFRRconfig(f'router isis {domain}', stop_section='^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']) @@ -414,9 +437,114 @@ 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}', endsection='^exit') + tmp = self.getFRRconfig(f'router isis {domain}', stop_section='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' topology {topology}', tmp) + def test_isis_11_srv6(self): + locator = "TEST" + interface = 'lo' + srv6_iface = 'dum6' + + # The dummy interface used to install SRv6 SIDs in the Linux data plane + self.cli_set(['interfaces', 'dummy', srv6_iface]) + + self.cli_set(base_path + ['net', net]) + self.cli_set(base_path + ['interface', interface]) + self.cli_set(base_path + ['segment-routing', 'srv6', 'locator', locator]) + self.cli_set(base_path + ['segment-routing', 'srv6', 'interface', srv6_iface]) + + # Commit main ISIS changes + self.cli_commit() + + # Verify main ISIS changes + tmp = self.getFRRconfig(f'router isis {domain}', stop_section='^exit') + self.assertIn(f' net {net}', tmp) + self.assertIn(f' segment-routing srv6', tmp) + self.assertIn(f' locator {locator}', tmp) + + # Commit for isis + self.cli_commit() + + def test_isis_12_frr_interface_lfa_remotelfa(self): + interface = 'eth0' + rla_metric = '10' + frr_interface_base_path = base_path + ['interface', interface, 'fast-reroute'] + self.cli_set(base_path + ['net', net]) + self.cli_set(base_path + ['interface', interface]) + self.cli_set(frr_interface_base_path + ['lfa', 'level-1', 'enable']) + self.cli_set(frr_interface_base_path + ['lfa', 'level-1', 'exclude', + 'interface', interface]) + self.cli_set(frr_interface_base_path + ['remote-lfa', 'level-1', + 'maximum-metric', rla_metric]) + self.cli_set(frr_interface_base_path + ['remote-lfa', 'level-1', + 'tunnel', 'mpls-ldp']) + + # Commit main ISIS changes + self.cli_commit() + + # Verify interface ISIS changes + tmp = self.getFRRconfig(f'interface {interface}', stop_section='^exit') + self.assertIn(f' isis fast-reroute lfa level-1', tmp) + self.assertIn(f' isis fast-reroute lfa level-1 exclude interface {interface}', tmp) + self.assertIn(f' isis fast-reroute remote-lfa maximum-metric {rla_metric} level-1', tmp) + self.assertIn(f' isis fast-reroute remote-lfa tunnel mpls-ldp level-1', tmp) + + def test_isis_13_frr_interface_tilfa(self): + interface = 'eth0' + frr_interface_base_path = base_path + ['interface', interface, 'fast-reroute'] + self.cli_set(base_path + ['net', net]) + self.cli_set(base_path + ['interface', interface]) + self.cli_set(frr_interface_base_path + ['ti-lfa', 'level-1', 'node-protection', + 'link-fallback']) + + # Commit main ISIS changes + self.cli_commit() + + # Verify interface ISIS changes + tmp = self.getFRRconfig(f'interface {interface}', stop_section='^exit') + self.assertIn(f' isis fast-reroute ti-lfa level-1 node-protection link-fallback', tmp) + + def test_isis_14_segment_routing_srv6_advanced(self): + # Configure system SRv6 locator and interface + locator = 'TEST' + sr_base = ['protocols', 'segment-routing'] + self.cli_set(sr_base + ['srv6', 'locator', 'TEST', 'prefix', '2001:db8::/64']) + self.cli_set(sr_base + ['interface', 'lo']) + + # The dummy interface used to install SRv6 SIDs in the Linux data plane + dum_iface = 'dum6' + self.cli_set(['interfaces', 'dummy', dum_iface]) + + # Set a basic IS-IS config + self.cli_set(base_path + ['net', net]) + self.cli_set(base_path + ['interface', 'lo']) + + # Configure IS-IS SRv6 + srv6_base_path = base_path + ['segment-routing', 'srv6'] + self.cli_set(srv6_base_path + ['locator', locator]) + self.cli_set(srv6_base_path + ['interface', dum_iface]) + self.cli_commit() + + tmp = self.getFRRconfig(f'router isis {domain}', stop_section='^exit') + self.assertIn(' segment-routing srv6', tmp) + self.assertIn(f' locator {locator}', tmp) + self.assertIn(f' interface {dum_iface}', tmp) + + # Test node-msd configuration + self.cli_set(srv6_base_path + ['node-msd', 'max-end-d', '40']) + self.cli_set(srv6_base_path + ['node-msd', 'max-end-pop', '50']) + self.cli_set(srv6_base_path + ['node-msd', 'max-h-encaps', '60']) + self.cli_set(srv6_base_path + ['node-msd', 'max-segs-left', '70']) + self.cli_commit() + + tmp = self.getFRRconfig(f'router isis {domain}', stop_section='^exit') + self.assertIn(' node-msd', tmp) + self.assertIn(' max-end-d 40', tmp) + self.assertIn(' max-end-pop 50', tmp) + self.assertIn(' max-h-encaps 60', tmp) + self.assertIn(' max-segs-left 70', tmp) + + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_protocols_mpls.py b/smoketest/scripts/cli/test_protocols_mpls.py index 3840c24f4..aa79d36b9 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-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,7 +17,6 @@ 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 @@ -77,8 +76,6 @@ class TestProtocolsMPLS(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 def tearDown(self): self.cli_delete(base_path) @@ -86,6 +83,8 @@ class TestProtocolsMPLS(VyOSUnitTestSHIM.TestCase): # check process health and continuity self.assertEqual(self.daemon_pid, process_named_running(ldpd_daemon)) + # always forward to base class + super().tearDown() def test_mpls_basic(self): router_id = '1.2.3.4' @@ -109,14 +108,14 @@ class TestProtocolsMPLS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Validate configuration - frrconfig = self.getFRRconfig('mpls ldp', endsection='^exit') + frrconfig = self.getFRRconfig('mpls ldp', stop_section='^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') + afiv4_config = self.getFRRconfig('mpls ldp', stop_section='^exit', + start_subsection=' address-family ipv4', + stop_subsection='^ 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) @@ -145,23 +144,23 @@ class TestProtocolsMPLS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Validate configuration - frrconfig = self.getFRRconfig('mpls ldp', endsection='^exit') + frrconfig = self.getFRRconfig('mpls ldp', stop_section='^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') + afiv4_config = self.getFRRconfig('mpls ldp', stop_section='^exit', + start_subsection=' address-family ipv4', + stop_subsection='^ 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') + afiv6_config = self.getFRRconfig('mpls ldp', stop_section='^exit', + start_subsection=' address-family ipv6', + stop_subsection='^ 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) @@ -175,13 +174,13 @@ class TestProtocolsMPLS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Validate AFI IPv4 - afiv4_config = self.getFRRconfig('mpls ldp', endsection='^exit', - substring=' address-family ipv4', - endsubsection='^ exit-address-family') + afiv4_config = self.getFRRconfig('mpls ldp', stop_section='^exit', + start_subsection=' address-family ipv4', + stop_subsection='^ exit-address-family') # Validate AFI IPv6 - afiv6_config = self.getFRRconfig('mpls ldp', endsection='^exit', - substring=' address-family ipv6', - endsubsection='^ exit-address-family') + afiv6_config = self.getFRRconfig('mpls ldp', stop_section='^exit', + start_subsection=' address-family ipv6', + stop_subsection='^ exit-address-family') # Check deleted 'disable-establish-hello' option per interface for interface in interfaces: self.assertIn(f' interface {interface}', afiv4_config) @@ -191,4 +190,4 @@ class TestProtocolsMPLS(VyOSUnitTestSHIM.TestCase): if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_protocols_nhrp.py b/smoketest/scripts/cli/test_protocols_nhrp.py index 73a760945..6a4e12f80 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-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -38,6 +38,8 @@ class TestProtocolsNHRP(VyOSUnitTestSHIM.TestCase): self.cli_delete(nhrp_path) self.cli_delete(tunnel_path) self.cli_commit() + # always forward to base class + super().tearDown() def test_01_nhrp_config(self): tunnel_if = "tun100" @@ -103,7 +105,7 @@ class TestProtocolsNHRP(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig(f'interface {tunnel_if}', endsection='^exit') + frrconfig = self.getFRRconfig(f'interface {tunnel_if}', stop_section='^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) @@ -139,4 +141,4 @@ class TestProtocolsNHRP(VyOSUnitTestSHIM.TestCase): self.assertTrue(process_named_running(PROCESS_NAME)) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_protocols_openfabric.py b/smoketest/scripts/cli/test_protocols_openfabric.py index 323b6cd74..c1e6f6598 100644 --- a/smoketest/scripts/cli/test_protocols_openfabric.py +++ b/smoketest/scripts/cli/test_protocols_openfabric.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,7 +17,6 @@ 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 @@ -42,8 +41,6 @@ class TestProtocolsOpenFabric(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 def tearDown(self): self.cli_delete(base_path) @@ -51,6 +48,8 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase): # check process health and continuity self.assertEqual(self.daemon_pid, process_named_running(openfabric_daemon)) + # always forward to base class + super().tearDown() def openfabric_base_config(self): self.cli_set(['interfaces', 'dummy', dummy_if]) @@ -79,14 +78,14 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - tmp = self.getFRRconfig(f'router openfabric {domain}', endsection='^exit') + tmp = self.getFRRconfig(f'router openfabric {domain}', stop_section='^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}', endsection='^exit') + tmp = self.getFRRconfig(f'interface {dummy_if}', stop_section='^exit') self.assertIn(f' ip router openfabric {domain}', tmp) self.assertIn(f' ipv6 router openfabric {domain}', tmp) @@ -105,12 +104,12 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR openfabric configuration - tmp = self.getFRRconfig(f'router openfabric {domain}', endsection='^exit') + tmp = self.getFRRconfig(f'router openfabric {domain}', stop_section='^exit') self.assertIn(f'router openfabric {domain}', tmp) self.assertIn(f' net {net}', tmp) # Verify interface configuration - tmp = self.getFRRconfig(f'interface {interface}', endsection='^exit') + tmp = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(f' ip router openfabric {domain}', tmp) # for lo interface 'openfabric passive' is implied self.assertIn(f' openfabric passive', tmp) @@ -118,6 +117,7 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase): def test_openfabric_03_password(self): password = 'foo' + md5_password = 'secret_md5_hash' self.openfabric_base_config() @@ -141,13 +141,30 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - tmp = self.getFRRconfig(f'router openfabric {domain}', endsection='^exit') + tmp = self.getFRRconfig(f'router openfabric {domain}', stop_section='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' domain-password clear {password}', tmp) - tmp = self.getFRRconfig(f'interface {dummy_if}', endsection='^exit') + tmp = self.getFRRconfig(f'interface {dummy_if}', stop_section='^exit') self.assertIn(f' openfabric password clear {password}-{dummy_if}', tmp) + # Switch to MD5 passwords - delete plaintext passwords first + self.cli_delete(path + ['domain-password', 'plaintext-password']) + self.cli_delete(path + ['interface', dummy_if, 'password', 'plaintext-password']) + + self.cli_set(path + ['domain-password', 'md5', md5_password]) + self.cli_set(path + ['interface', dummy_if, 'password', 'md5', md5_password]) + + # Commit all changes + self.cli_commit() + + # Verify all changes + tmp = self.getFRRconfig(f'router openfabric {domain}', stop_section='^exit') + self.assertIn(f' domain-password md5 {md5_password}', tmp) + + tmp = self.getFRRconfig(f'interface {dummy_if}', stop_section='^exit') + self.assertIn(f' openfabric password md5 {md5_password}', tmp) + def test_openfabric_multiple_domains(self): domain_2 = 'VyOS_2' interface = 'dum5678' @@ -169,21 +186,21 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR openfabric configuration - tmp = self.getFRRconfig(f'router openfabric {domain}', endsection='^exit') + tmp = self.getFRRconfig(f'router openfabric {domain}', stop_section='^exit') self.assertIn(f'router openfabric {domain}', tmp) self.assertIn(f' net {net}', tmp) - tmp = self.getFRRconfig(f'router openfabric {domain_2}', endsection='^exit') + tmp = self.getFRRconfig(f'router openfabric {domain_2}', stop_section='^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}', endsection='^exit') + tmp = self.getFRRconfig(f'interface {dummy_if}', stop_section='^exit') self.assertIn(f' ip router openfabric {domain}', tmp) self.assertIn(f' ipv6 router openfabric {domain}', tmp) - tmp = self.getFRRconfig(f'interface {interface}', endsection='^exit') + tmp = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(f' ip router openfabric {domain_2}', tmp) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_protocols_ospf.py b/smoketest/scripts/cli/test_protocols_ospf.py index ea55fa031..299f518a8 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-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,7 +18,6 @@ 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 @@ -46,8 +45,6 @@ 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): @@ -59,11 +56,13 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() - frrconfig = self.getFRRconfig('router ospf', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf', stop_section='^exit') self.assertNotIn(f'router ospf', frrconfig) # check process health and continuity self.assertEqual(self.daemon_pid, process_named_running(ospf_daemon)) + # always forward to base class + super().tearDown() def test_ospf_01_defaults(self): # commit changes @@ -71,7 +70,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf', stop_section='^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 @@ -99,7 +98,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf', stop_section='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' compatible rfc1583', frrconfig) self.assertIn(f' auto-cost reference-bandwidth {bandwidth}', frrconfig) @@ -130,7 +129,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf', stop_section='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults for ptotocol in protocols: @@ -150,7 +149,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf', stop_section='^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) @@ -160,7 +159,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf', stop_section='^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): @@ -201,7 +200,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf', stop_section='^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 @@ -224,7 +223,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['distance', 'ospf', 'inter-area', inter_area]) self.cli_commit() - frrconfig = self.getFRRconfig('router ospf', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf', stop_section='^exit') self.assertIn(f' distance ospf intra-area {intra_area} inter-area {inter_area} external {external}', frrconfig) # https://github.com/FRRouting/frr/issues/17011 @@ -247,7 +246,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf', stop_section='^exit') self.assertIn(f'router ospf', frrconfig) for neighbor in neighbors: self.assertIn(f' neighbor {neighbor} priority {priority} poll-interval {poll_interval}', frrconfig) # default @@ -255,21 +254,42 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): def test_ospf_07_redistribute(self): metric = '15' metric_type = '1' - redistribute = ['babel', 'bgp', 'connected', 'isis', 'kernel', 'nhrp', 'rip', 'static'] + table_id = '21' + redistribute = [ + 'babel', + 'bgp', + 'connected', + 'isis', + 'kernel', + 'nhrp', + 'rip', + 'static', + 'table', + ] for protocol in redistribute: - self.cli_set(base_path + ['redistribute', protocol, 'metric', metric]) - self.cli_set(base_path + ['redistribute', protocol, 'route-map', route_map]) - self.cli_set(base_path + ['redistribute', protocol, 'metric-type', metric_type]) + redistr_base = base_path + ['redistribute', protocol] + if protocol == 'table': + redistr_base += [table_id] + self.cli_set(redistr_base + ['metric', metric]) + self.cli_set(redistr_base + ['route-map', route_map]) + self.cli_set(redistr_base + ['metric-type', metric_type]) # commit changes self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf', stop_section='^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) + if protocol == 'table': + protocolstr = f'table-direct {table_id}' + else: + protocolstr = protocol + self.assertIn( + f' redistribute {protocolstr} metric {metric} metric-type {metric_type} route-map {route_map}', + frrconfig, + ) def test_ospf_08_virtual_link(self): networks = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'] @@ -290,11 +310,16 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): for network in networks: self.cli_set(base_path + ['area', area, 'network', network]) + # FRR requires router to be ABR for virtual-link to work + self.cli_set(base_path + ['area', '0', 'network', '192.178.0.0/16']) + self.cli_set(['interfaces', 'dummy', dummy_if, 'address', '172.16.0.9/12']) + self.cli_set(['interfaces', 'dummy', dummy_if, 'address', '192.178.0.9/16']) + # commit changes self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf', stop_section='^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} retransmit-window {window_default} transmit-delay {transmit} dead-interval {dead}', frrconfig) @@ -326,13 +351,13 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): # commit changes self.cli_commit() - frrconfig = self.getFRRconfig('router ospf', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf', stop_section='^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}', endsection='^exit') + config = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(f'interface {interface}', config) self.assertIn(f' ip ospf authentication-key {password}', config) self.assertIn(f' ip ospf bfd', config) @@ -350,7 +375,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): for interface in interfaces: # T5467: It must also be removed from FRR config - frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') + frrconfig = self.getFRRconfig(f'interface {interface}', stop_section='^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) @@ -371,11 +396,11 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf', stop_section='^exit') self.assertIn(f'router ospf', frrconfig) for interface in interfaces: - config = self.getFRRconfig(f'interface {interface}', endsection='^exit') + config = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(f'interface {interface}', config) self.assertIn(f' ip ospf area {area}', config) @@ -398,17 +423,17 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf', stop_section='^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}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router ospf vrf {vrf}', stop_section='^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}', endsection='^exit') + frrconfig = self.getFRRconfig(f'interface {vrf_iface}', stop_section='^exit') self.assertIn(f'interface {vrf_iface}', frrconfig) self.assertIn(f' ip ospf area {area}', frrconfig) @@ -418,7 +443,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # T5467: It must also be removed from FRR config - frrconfig = self.getFRRconfig(f'interface {vrf_iface}', endsection='^exit') + frrconfig = self.getFRRconfig(f'interface {vrf_iface}', stop_section='^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) @@ -444,7 +469,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf', stop_section='^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) @@ -477,7 +502,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - frrconfig = self.getFRRconfig('router ospf', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf', stop_section='^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) @@ -495,7 +520,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify main OSPF changes - frrconfig = self.getFRRconfig('router ospf', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf', stop_section='^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) @@ -508,7 +533,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): for interface in interfaces: # Verify interface changes for holddown - config = self.getFRRconfig(f'interface {interface}', endsection='^exit') + config = self.getFRRconfig(f'interface {interface}', stop_section='^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) @@ -522,7 +547,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): for interface in interfaces: # Verify interface changes for disable - config = self.getFRRconfig(f'interface {interface}', endsection='^exit') + config = self.getFRRconfig(f'interface {interface}', stop_section='^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) @@ -544,7 +569,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf', stop_section='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' capability opaque', frrconfig) self.assertIn(f' graceful-restart grace-period {period}', frrconfig) @@ -570,9 +595,70 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit', empty_retry=60) + frrconfig = self.getFRRconfig('router ospf', stop_section='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' network {network} area {area1}', frrconfig) + def test_ospf_18_area_translate_no_summary(self): + area = '11' + area_type = 'nssa' + network = '100.64.0.0/10' + + self.cli_set(base_path + ['area', area, 'area-type', area_type, 'no-summary']) + self.cli_set(base_path + ['area', area, 'area-type', area_type, 'translate', 'never']) + self.cli_set(base_path + ['area', area, 'network', network]) + + # commit changes + self.cli_commit() + + # Verify FRR ospfd configuration + frrconfig = self.getFRRconfig('router ospf', stop_section='^exit') + self.assertIn(f'router ospf', frrconfig) + self.assertIn(f' area {area} {area_type} translate-never no-summary', frrconfig) + self.assertIn(f' network {network} area {area}', frrconfig) + + def test_ospf_19_authentication(self): + md5_key = 'vyosMD5' + md5_id = '10' + plaintext_key = 'vyos123' + + self.cli_set(base_path + ['area', '0']) + self.cli_set(base_path + ['interface', dummy_if, 'authentication', 'md5', 'key-id', md5_id, 'md5-key', md5_key]) + self.cli_commit() + + # Verify FRR ospfd configuration + frrconfig = self.getFRRconfig(f'interface {dummy_if}', stop_section='^exit') + self.assertIn( ' ip ospf authentication message-digest', frrconfig) + self.assertIn(f' ip ospf message-digest-key {md5_id} md5 {md5_key}', frrconfig) + + self.cli_set(base_path + ['interface', dummy_if, 'authentication', 'plaintext-password', plaintext_key]) + # FRR only allows a single authentication mode (MD5, NULL or plaintext) at a time + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['interface', dummy_if, 'authentication', 'md5']) + self.cli_commit() + + # Verify FRR ospfd configuration + frrconfig = self.getFRRconfig(f'interface {dummy_if}', stop_section='^exit') + self.assertNotIn( ' ip ospf authentication message-digest', frrconfig) + self.assertNotIn(f' ip ospf message-digest-key {md5_id} md5 {md5_key}', frrconfig) + self.assertIn( ' ip ospf authentication', frrconfig) + self.assertIn(f' ip ospf authentication-key {plaintext_key}', frrconfig) + + self.cli_set(base_path + ['interface', dummy_if, 'authentication', 'null']) + # FRR only allows a single authentication mode (MD5, NULL or plaintext) at a time + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['interface', dummy_if, 'authentication', 'plaintext-password']) + self.cli_commit() + + # Verify FRR ospfd configuration + frrconfig = self.getFRRconfig(f'interface {dummy_if}', stop_section='^exit') + self.assertNotIn( ' ip ospf authentication message-digest', frrconfig) + self.assertNotIn(f' ip ospf message-digest-key {md5_id} md5 {md5_key}', frrconfig) + self.assertNotRegex(r'^ ip ospf authentication$', frrconfig) + self.assertNotIn(f' ip ospf authentication-key {plaintext_key}', frrconfig) + self.assertIn(' ip ospf authentication null', frrconfig) + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_protocols_ospfv3.py b/smoketest/scripts/cli/test_protocols_ospfv3.py index 5da4c7c98..db6602db8 100755 --- a/smoketest/scripts/cli/test_protocols_ospfv3.py +++ b/smoketest/scripts/cli/test_protocols_ospfv3.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,7 +17,6 @@ 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 @@ -45,8 +44,6 @@ 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): @@ -57,11 +54,13 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf6', stop_section='^exit') self.assertNotIn(f'router ospf6', frrconfig) # check process health and continuity self.assertEqual(self.daemon_pid, process_named_running(ospf6_daemon)) + # always forward to base class + super().tearDown() def test_ospfv3_01_basic(self): seq = '10' @@ -84,7 +83,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf6', stop_section='^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) @@ -92,7 +91,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}', endsection='^exit') + if_config = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(f'ipv6 ospf6 area {default_area}', if_config) self.cli_delete(['policy', 'access-list6', acl_name]) @@ -113,7 +112,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf6', stop_section='^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) @@ -137,7 +136,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf6', stop_section='^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) @@ -168,13 +167,13 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf6', stop_section='^exit') self.assertIn(f'router ospf6', frrconfig) cost = '100' priority = '10' for interface in interfaces: - if_config = self.getFRRconfig(f'interface {interface}', endsection='^exit') + if_config = self.getFRRconfig(f'interface {interface}', stop_section='^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) @@ -191,7 +190,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() for interface in interfaces: - if_config = self.getFRRconfig(f'interface {interface}', endsection='^exit') + if_config = self.getFRRconfig(f'interface {interface}', stop_section='^exit') # There should be no OSPF6 configuration at all after interface removal self.assertNotIn(f' ipv6 ospf6', if_config) @@ -207,7 +206,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf6', stop_section='^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) @@ -233,7 +232,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf6', stop_section='^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) @@ -253,7 +252,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf6', stop_section='^exit') self.assertIn(f'router ospf6', frrconfig) self.assertIn(f' default-information originate metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig) @@ -262,7 +261,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf6', stop_section='^exit') self.assertIn(f' default-information originate always metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig) @@ -288,15 +287,15 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf6', stop_section='^exit') self.assertIn(f'router ospf6', frrconfig) self.assertIn(f' ospf6 router-id {router_id}', frrconfig) - frrconfig = self.getFRRconfig(f'interface {vrf_iface}', endsection='^exit') + frrconfig = self.getFRRconfig(f'interface {vrf_iface}', stop_section='^exit') self.assertIn(f'interface {vrf_iface}', frrconfig) self.assertIn(f' ipv6 ospf6 bfd', frrconfig) - frrconfig = self.getFRRconfig(f'router ospf6 vrf {vrf}', endsection='^exit') + frrconfig = self.getFRRconfig(f'router ospf6 vrf {vrf}', stop_section='^exit') self.assertIn(f'router ospf6 vrf {vrf}', frrconfig) self.assertIn(f' ospf6 router-id {router_id_vrf}', frrconfig) @@ -306,7 +305,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # T5467: It must also be removed from FRR config - frrconfig = self.getFRRconfig(f'interface {vrf_iface}', endsection='^exit') + frrconfig = self.getFRRconfig(f'interface {vrf_iface}', stop_section='^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) @@ -332,7 +331,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') + frrconfig = self.getFRRconfig('router ospf6', stop_section='^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) @@ -342,4 +341,4 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.assertIn(f' graceful-restart helper enable {router_id}', frrconfig) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_protocols_pim.py b/smoketest/scripts/cli/test_protocols_pim.py index cc62769b3..cb04c5bb2 100755 --- a/smoketest/scripts/cli/test_protocols_pim.py +++ b/smoketest/scripts/cli/test_protocols_pim.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,7 +17,6 @@ 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 @@ -34,8 +33,6 @@ class TestProtocolsPIM(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 def tearDown(self): # pimd process must be running @@ -46,6 +43,8 @@ class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase): # pimd process must be stopped by now self.assertFalse(process_named_running(pim_daemon)) + # always forward to base class + super().tearDown() def test_01_pim_basic(self): rp = '127.0.0.1' @@ -68,11 +67,11 @@ class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR pimd configuration - frrconfig = self.getFRRconfig('router pim', endsection='^exit') + frrconfig = self.getFRRconfig('router pim', stop_section='^exit') self.assertIn(f' rp {rp} {group}', frrconfig) for interface in interfaces: - frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') + frrconfig = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(f'interface {interface}', frrconfig) self.assertIn(f' ip pim', frrconfig) self.assertIn(f' ip pim bfd', frrconfig) @@ -119,7 +118,7 @@ class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR pimd configuration - frrconfig = self.getFRRconfig('router pim', endsection='^exit') + frrconfig = self.getFRRconfig('router pim', stop_section='^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) @@ -185,7 +184,7 @@ class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase): self.assertIn(f'ip igmp watermark-warn {watermark_warning}', frrconfig) for interface in interfaces: - frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') + frrconfig = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(f'interface {interface}', frrconfig) self.assertIn(f' ip igmp', frrconfig) self.assertIn(f' ip igmp version {version}', frrconfig) @@ -200,4 +199,4 @@ class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase): self.assertIn(f' ip igmp join-group {join}', frrconfig) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_protocols_pim6.py b/smoketest/scripts/cli/test_protocols_pim6.py index 4ed8fcf7a..f1c35e058 100755 --- a/smoketest/scripts/cli/test_protocols_pim6.py +++ b/smoketest/scripts/cli/test_protocols_pim6.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,9 +17,8 @@ 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 @@ -36,8 +35,6 @@ class TestProtocolsPIMv6(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 def tearDown(self): self.cli_delete(base_path) @@ -45,6 +42,8 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): # check process health and continuity self.assertEqual(self.daemon_pid, process_named_running(pim6_daemon)) + # always forward to base class + super().tearDown() def test_pim6_01_mld_simple(self): # commit changes @@ -56,7 +55,7 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): # Verify FRR pim6d configuration for interface in interfaces: - config = self.getFRRconfig(f'interface {interface}', endsection='^exit') + config = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(f'interface {interface}', config) self.assertIn(f' ipv6 mld', config) self.assertNotIn(f' ipv6 mld version 1', config) @@ -69,7 +68,7 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): # Verify FRR pim6d configuration for interface in interfaces: - config = self.getFRRconfig(f'interface {interface}', endsection='^exit') + config = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(f'interface {interface}', config) self.assertIn(f' ipv6 mld', config) self.assertIn(f' ipv6 mld version 1', config) @@ -92,7 +91,7 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): # Verify FRR pim6d configuration for interface in interfaces: - config = self.getFRRconfig(f'interface {interface}', endsection='^exit') + config = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(f'interface {interface}', config) self.assertIn(f' ipv6 mld join-group ff18::1234', config) @@ -104,7 +103,7 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): # Verify FRR pim6d configuration for interface in interfaces: - config = self.getFRRconfig(f'interface {interface}', endsection='^exit') + config = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(f'interface {interface}', config) self.assertIn(f' ipv6 mld join-group ff38::5678 2001:db8::5678', config) @@ -132,14 +131,14 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR pim6d configuration - config = self.getFRRconfig('router pim6', endsection='^exit') + config = self.getFRRconfig('router pim6', stop_section='^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}', endsection='^exit') + config = self.getFRRconfig(f'interface {interface}', stop_section='^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) @@ -147,4 +146,4 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): self.assertIn(f' ipv6 pim passive', config) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_protocols_rip.py b/smoketest/scripts/cli/test_protocols_rip.py index 27b543803..71d3e183c 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-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,7 +17,6 @@ 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 @@ -40,8 +39,6 @@ class TestProtocolsRIP(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 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']) @@ -69,11 +66,13 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() - frrconfig = self.getFRRconfig('router rip', endsection='^exit') + frrconfig = self.getFRRconfig('router rip', stop_section='^exit') self.assertNotIn(f'router rip', frrconfig) # check process health and continuity self.assertEqual(self.daemon_pid, process_named_running(rip_daemon)) + # always forward to base class + super().tearDown() def test_rip_01_parameters(self): distance = '40' @@ -119,7 +118,7 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ripd configuration - frrconfig = self.getFRRconfig('router rip', endsection='^exit') + frrconfig = self.getFRRconfig('router rip', stop_section='^exit') self.assertIn(f'router rip', frrconfig) self.assertIn(f' distance {distance}', frrconfig) self.assertIn(f' default-information originate', frrconfig) @@ -178,12 +177,12 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR configuration - frrconfig = self.getFRRconfig('router rip', endsection='^exit') + frrconfig = self.getFRRconfig('router rip', stop_section='^exit') self.assertIn(f'version {tx_version}', frrconfig) - frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') + frrconfig = self.getFRRconfig(f'interface {interface}', stop_section='^exit') self.assertIn(f' ip rip receive version {rx_version}', frrconfig) self.assertIn(f' ip rip send version {tx_version}', frrconfig) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_protocols_ripng.py b/smoketest/scripts/cli/test_protocols_ripng.py index d2066b825..4dc3e2cfa 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-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,7 +17,6 @@ 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 @@ -41,8 +40,6 @@ class TestProtocolsRIPng(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 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']) @@ -69,11 +66,13 @@ class TestProtocolsRIPng(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() - frrconfig = self.getFRRconfig('router ripng', endsection='^exit') + frrconfig = self.getFRRconfig('router ripng', stop_section='^exit') self.assertNotIn(f'router ripng', frrconfig) # check process health and continuity self.assertEqual(self.daemon_pid, process_named_running(ripng_daemon)) + # always forward to base class + super().tearDown() def test_ripng_01_parameters(self): metric = '8' @@ -116,7 +115,7 @@ class TestProtocolsRIPng(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ripng', endsection='^exit') + frrconfig = self.getFRRconfig('router ripng', stop_section='^exit') self.assertIn(f'router ripng', frrconfig) self.assertIn(f' default-information originate', frrconfig) self.assertIn(f' default-metric {metric}', frrconfig) @@ -163,4 +162,4 @@ class TestProtocolsRIPng(VyOSUnitTestSHIM.TestCase): self.assertNotIn(zebra_route_map, frrconfig) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_protocols_rpki.py b/smoketest/scripts/cli/test_protocols_rpki.py index 0addf7fee..1345f254c 100755 --- a/smoketest/scripts/cli/test_protocols_rpki.py +++ b/smoketest/scripts/cli/test_protocols_rpki.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,7 +17,6 @@ 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 @@ -25,6 +24,11 @@ from vyos.utils.file import read_file from vyos.utils.process import process_named_running base_path = ['protocols', 'rpki'] +base_frr_config_args = {'start_section': 'rpki', 'stop_section': '^exit'} +vrf = 'blue' +vrf_path = ['vrf', 'name', vrf] +vrf_frr_config_args = {'start_section': f'vrf {vrf}', 'stop_section':'^exit-vrf', + 'start_subsection': ' rpki', 'stop_subsection': '^ exit'} rpki_key_name = 'rpki-smoketest' rpki_key_type = 'ssh-rsa' @@ -112,18 +116,23 @@ class TestProtocolsRPKI(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 + cls.cli_delete(cls, vrf_path) def tearDown(self): self.cli_delete(base_path) + self.cli_delete(vrf_path) self.cli_commit() - frrconfig = self.getFRRconfig('rpki', endsection='^exit') + frrconfig = self.getFRRconfig(**base_frr_config_args) + self.assertNotIn(f'rpki', frrconfig) + + frrconfig = self.getFRRconfig(**vrf_frr_config_args) self.assertNotIn(f'rpki', frrconfig) # check process health and continuity self.assertEqual(self.daemon_pid, process_named_running(bgp_daemon)) + # always forward to base class + super().tearDown() def test_rpki(self): expire_interval = '3600' @@ -144,27 +153,33 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): }, } - self.cli_set(base_path + ['expire-interval', expire_interval]) - self.cli_set(base_path + ['polling-period', polling_period]) - self.cli_set(base_path + ['retry-interval', retry_interval]) + for test_set in [ {'path': base_path, 'frrargs': base_frr_config_args}, + {'path': vrf_path + base_path, 'frrargs': vrf_frr_config_args} ]: - for peer, peer_config in cache.items(): - self.cli_set(base_path + ['cache', peer, 'port', peer_config['port']]) - self.cli_set(base_path + ['cache', peer, 'preference', peer_config['preference']]) + if 'vrf' in test_set['path']: + self.cli_set(vrf_path + ['table', '1000']) - # commit changes - self.cli_commit() + self.cli_set(test_set['path'] + ['expire-interval', expire_interval]) + self.cli_set(test_set['path'] + ['polling-period', polling_period]) + self.cli_set(test_set['path'] + ['retry-interval', retry_interval]) + + for peer, peer_config in cache.items(): + self.cli_set(test_set['path'] + ['cache', peer, 'port', peer_config['port']]) + self.cli_set(test_set['path'] + ['cache', peer, 'preference', peer_config['preference']]) + + # commit changes + self.cli_commit() - # Verify FRR configuration - 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) + # Verify FRR configuration + frrconfig = self.getFRRconfig(**test_set['frrargs']) + 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) - for peer, peer_config in cache.items(): - port = peer_config['port'] - preference = peer_config['preference'] - self.assertIn(f'rpki cache tcp {peer} {port} preference {preference}', frrconfig) + for peer, peer_config in cache.items(): + port = peer_config['port'] + preference = peer_config['preference'] + self.assertIn(f'rpki cache tcp {peer} {port} preference {preference}', frrconfig) def test_rpki_ssh(self): polling = '7200' @@ -185,28 +200,34 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): 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]) - for cache_name, cache_config in cache.items(): - self.cli_set(base_path + ['cache', cache_name, 'port', cache_config['port']]) - self.cli_set(base_path + ['cache', cache_name, 'preference', cache_config['preference']]) - self.cli_set(base_path + ['cache', cache_name, 'ssh', 'username', cache_config['username']]) - self.cli_set(base_path + ['cache', cache_name, 'ssh', 'key', rpki_key_name]) + for test_set in [ {'path': base_path, 'frrargs': base_frr_config_args}, + {'path': vrf_path + base_path, 'frrargs': vrf_frr_config_args} ]: - # commit changes - self.cli_commit() + if 'vrf' in test_set['path']: + self.cli_set(vrf_path + ['table', '1000']) + + for cache_name, cache_config in cache.items(): + self.cli_set(test_set['path'] + ['cache', cache_name, 'port', cache_config['port']]) + self.cli_set(test_set['path'] + ['cache', cache_name, 'preference', cache_config['preference']]) + self.cli_set(test_set['path'] + ['cache', cache_name, 'ssh', 'username', cache_config['username']]) + self.cli_set(test_set['path'] + ['cache', cache_name, 'ssh', 'key', rpki_key_name]) + + # commit changes + self.cli_commit() - # Verify FRR configuration - 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 ssh {cache_name} {port} {username} /run/frr/id_rpki_{cache_name} /run/frr/id_rpki_{cache_name}.pub preference {preference}', frrconfig) + # Verify FRR configuration + frrconfig = self.getFRRconfig(**test_set['frrargs']) + 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 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}') - self.assertIn(rpki_ssh_key.replace('\n',''), tmp) - tmp = read_file(f'/run/frr/id_rpki_{cache_name}.pub') - self.assertIn(rpki_ssh_pub.replace('\n',''), tmp) + # Verify content of SSH keys + tmp = read_file(f'/run/frr/id_rpki_{cache_name}') + self.assertIn(rpki_ssh_key.replace('\n',''), tmp) + tmp = read_file(f'/run/frr/id_rpki_{cache_name}.pub') + self.assertIn(rpki_ssh_pub.replace('\n',''), tmp) # Change OpenSSH key and verify it was properly written to filesystem self.cli_set(['pki', 'openssh', rpki_key_name, 'private', 'key', rpki_ssh_key_replacement.replace('\n','')]) @@ -214,17 +235,21 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): # commit changes self.cli_commit() - 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 ssh {cache_name} {port} {username} /run/frr/id_rpki_{cache_name} /run/frr/id_rpki_{cache_name}.pub preference {preference}', frrconfig) + for test_set in [ {'path': base_path, 'frrargs': base_frr_config_args}, + {'path': vrf_path + base_path, 'frrargs': vrf_frr_config_args} ]: - # Verify content of SSH keys - tmp = read_file(f'/run/frr/id_rpki_{cache_name}') - self.assertIn(rpki_ssh_key_replacement.replace('\n',''), tmp) - tmp = read_file(f'/run/frr/id_rpki_{cache_name}.pub') - self.assertIn(rpki_ssh_pub_replacement.replace('\n',''), tmp) + frrconfig = self.getFRRconfig(**test_set['frrargs']) + 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 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}') + self.assertIn(rpki_ssh_key_replacement.replace('\n',''), tmp) + tmp = read_file(f'/run/frr/id_rpki_{cache_name}.pub') + self.assertIn(rpki_ssh_pub_replacement.replace('\n',''), tmp) self.cli_delete(['pki', 'openssh']) @@ -240,13 +265,19 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): }, } - for peer, peer_config in cache.items(): - self.cli_set(base_path + ['cache', peer, 'port', peer_config['port']]) - self.cli_set(base_path + ['cache', peer, 'preference', peer_config['preference']]) + for test_set in [ {'path': base_path, 'frrargs': base_frr_config_args}, + {'path': vrf_path + base_path, 'frrargs': vrf_frr_config_args} ]: - # check validate() - preferences must be unique - with self.assertRaises(ConfigSessionError): - self.cli_commit() + if 'vrf' in test_set['path']: + self.cli_set(vrf_path + ['table', '1000']) + + for peer, peer_config in cache.items(): + self.cli_set(test_set['path'] + ['cache', peer, 'port', peer_config['port']]) + self.cli_set(test_set['path'] + ['cache', peer, 'preference', peer_config['preference']]) + + # check validate() - preferences must be unique + with self.assertRaises(ConfigSessionError): + self.cli_commit() def test_rpki_source_address(self): peer = '192.0.2.1' @@ -257,32 +288,39 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): 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) + for test_set in [ {'path': base_path, 'frrargs': base_frr_config_args}, + {'path': vrf_path + base_path, 'frrargs': vrf_frr_config_args} ]: - 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]) + if 'vrf' in test_set['path']: + self.cli_set(vrf_path + ['table', '1000']) - # 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() + # Configure a TCP cache server + self.cli_set(test_set['path'] + ['cache', peer, 'port', port]) + self.cli_set(test_set['path'] + ['cache', peer, 'preference', preference]) + self.cli_set(test_set['path'] + ['cache', peer, 'source-address', source_address]) + self.cli_commit() + + # Verify FRR configuration + frrconfig = self.getFRRconfig(**test_set['frrargs']) + 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(test_set['path'] + ['cache', peer, 'ssh', 'username', username]) + self.cli_set(test_set['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, - ) + # Verify FRR configuration + frrconfig = self.getFRRconfig(**test_set['frrargs']) + 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) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_protocols_segment-routing.py b/smoketest/scripts/cli/test_protocols_segment-routing.py index 94c808733..09d32445a 100755 --- a/smoketest/scripts/cli/test_protocols_segment-routing.py +++ b/smoketest/scripts/cli/test_protocols_segment-routing.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,7 +17,6 @@ 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 @@ -27,7 +26,6 @@ from vyos.utils.system import sysctl_read base_path = ['protocols', 'segment-routing'] - class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): @@ -38,8 +36,16 @@ class TestProtocolsSegmentRouting(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 + # create a VLAN interface for testing + cls.cli_set(cls, ['interfaces', 'ethernet', 'eth0', 'vif', '4000', + 'address', '192.168.40.1/24']) + cls.cli_commit(cls) + cls._interfaces = Section.interfaces('ethernet', vlan=True) + + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, ['interfaces', 'ethernet', 'eth0', 'vif', '4000']) + super(TestProtocolsSegmentRouting, cls).tearDownClass() def tearDown(self): self.cli_delete(base_path) @@ -47,9 +53,10 @@ class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase): # check process health and continuity self.assertEqual(self.daemon_pid, process_named_running(zebra_daemon)) + # always forward to base class + super().tearDown() def test_srv6(self): - interfaces = Section.interfaces('ethernet', vlan=False) locators = { 'foo1': {'prefix': '2001:a::/64'}, 'foo2': {'prefix': '2001:b::/64', 'usid': {}}, @@ -58,7 +65,7 @@ class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase): 'prefix': '2001:d::/48', 'block-len': '32', 'node-len': '16', - 'func-bits': '16', + 'func-bits': '12', 'usid': {}, 'format': 'usid-f3216', }, @@ -113,32 +120,44 @@ class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase): # verify() - SRv6 should be enabled on at least one interface! with self.assertRaises(ConfigSessionError): self.cli_commit() - for interface in interfaces: + for interface in self._interfaces: self.cli_set(base_path + ['interface', interface, 'srv6']) self.cli_commit() - for interface in interfaces: + for interface in self._interfaces: self.assertEqual( - sysctl_read(f'net.ipv6.conf.{interface}.seg6_enabled'), '1' + sysctl_read(['net', 'ipv6', 'conf', interface, 'seg6_enabled']), '1' ) self.assertEqual( - sysctl_read(f'net.ipv6.conf.{interface}.seg6_require_hmac'), '0' + sysctl_read(['net', 'ipv6', 'conf', interface, 'seg6_require_hmac']), '0' ) # default - frrconfig = self.getFRRconfig('segment-routing', endsection='^exit') + frrconfig = self.getFRRconfig('segment-routing', stop_section='^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') + block_len = ( + f' block-len {locator_config["block-len"]}' + if 'block-len' in locator_config + else '' + ) + node_len = ( + f' node-len {locator_config["node-len"]}' + if 'node-len' in locator_config + else '' + ) + func_bits = ( + f' func-bits {locator_config["func-bits"]}' + if 'func-bits' in locator_config + else '' + ) self.assertIn(f' locator {locator}', frrconfig) self.assertIn( - f' prefix {prefix} block-len {block_len} node-len {node_len} func-bits {func_bits}', + f' prefix {prefix}{block_len}{node_len}{func_bits}', frrconfig, ) @@ -147,44 +166,156 @@ class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase): if 'usid' in locator_config: self.assertIn(' behavior usid', frrconfig) + def test_srv6_encap_source_addr(self): + # Set an IPv6 address for SRv6 encapsulation source + source6 = '2001:db8::1' + + # SRv6 must be enabled on at least one interface + for interface in self._interfaces: + self.cli_set(base_path + ['interface', interface, 'srv6']) + + self.cli_set(base_path + ['srv6', 'encapsulation', 'source-address', source6]) + self.cli_commit() + + frrconfig = self.getFRRconfig('segment-routing', stop_section='^exit') + self.assertIn('segment-routing', frrconfig) + self.assertIn(' srv6', frrconfig) + self.assertIn(' encapsulation', frrconfig) + self.assertIn(f' source-address {source6}', frrconfig) + def test_srv6_sysctl(self): - interfaces = Section.interfaces('ethernet', vlan=False) # HMAC accept - for interface in interfaces: + for interface in self._interfaces: self.cli_set(base_path + ['interface', interface, 'srv6']) self.cli_set(base_path + ['interface', interface, 'srv6', 'hmac', 'ignore']) self.cli_commit() - for interface in interfaces: + for interface in self._interfaces: self.assertEqual( - sysctl_read(f'net.ipv6.conf.{interface}.seg6_enabled'), '1' + sysctl_read(['net', 'ipv6', 'conf', interface, 'seg6_enabled']), '1' ) self.assertEqual( - sysctl_read(f'net.ipv6.conf.{interface}.seg6_require_hmac'), '-1' + sysctl_read(['net', 'ipv6', 'conf', interface, 'seg6_require_hmac']), '-1' ) # ignore # HMAC drop - for interface in interfaces: + for interface in self._interfaces: self.cli_set(base_path + ['interface', interface, 'srv6']) self.cli_set(base_path + ['interface', interface, 'srv6', 'hmac', 'drop']) self.cli_commit() - for interface in interfaces: + for interface in self._interfaces: self.assertEqual( - sysctl_read(f'net.ipv6.conf.{interface}.seg6_enabled'), '1' + sysctl_read(['net', 'ipv6', 'conf', interface, 'seg6_enabled']), '1' ) self.assertEqual( - sysctl_read(f'net.ipv6.conf.{interface}.seg6_require_hmac'), '1' + sysctl_read(['net', 'ipv6', 'conf', interface, 'seg6_require_hmac']), '1' ) # drop # Disable SRv6 on first interface - first_if = interfaces[-1] + first_if = self._interfaces[-1] self.cli_delete(base_path + ['interface', first_if]) self.cli_commit() - self.assertEqual(sysctl_read(f'net.ipv6.conf.{first_if}.seg6_enabled'), '0') + self.assertEqual(sysctl_read(['net', 'ipv6', 'conf', first_if, 'seg6_enabled']), '0') + + def test_srte_database(self): + for protocol in ['isis', 'ospf']: + self.cli_set(base_path + ['traffic-engineering', 'database-import-protocol', protocol]) + # IS-IS and OSPF are mutually exclusive + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + # Add database-import-protocol for isis and check the config + for protocol in ['isis', 'ospf']: + self.cli_delete(base_path) + self.cli_set(base_path + ['traffic-engineering', 'database-import-protocol', protocol]) + self.cli_commit() + + frrconfig = self.getFRRconfig(f'segment-routing', stop_section='^exit') + self.assertIn('segment-routing', frrconfig) + self.assertIn(' traffic-eng', frrconfig) + self.assertIn(' mpls-te on', frrconfig) + self.assertIn(f' mpls-te import {protocol}', frrconfig) + + def test_srte_mpls_label(self): + # Add segment-list with an mpls value + mpls_label = '500' + segment_list = 'smoketest-mpls-only-segment-list' + index_value = '0' + + self.cli_set(base_path + ['traffic-engineering', 'segment-list', segment_list, + 'index', index_value, 'mpls', 'label', mpls_label]) + self.cli_commit() + + frrconfig = self.getFRRconfig(f'segment-routing', stop_section='^exit') + self.assertIn('segment-routing', frrconfig) + self.assertIn(' traffic-eng', frrconfig) + self.assertIn(' mpls-te on', frrconfig) + self.assertIn(f' segment-list {segment_list}', frrconfig) + self.assertIn(f' index {index_value} mpls label {mpls_label}', frrconfig) + + def test_srte_mpls_label_and_adjacency(self): + # Add segment-list with an mpls value and adjacency + mpls_label = '1000' + segment_list = 'smoketest-mpls-with-adjacency-segment-list' + index_value = '0' + + addresses = {'ipv4' : {'source_identifier' : '192.168.255.1', 'destination_identifier' : '192.168.255.2'}, + 'ipv6' : {'source_identifier' : '2003::1', 'destination_identifier' : '2003::2'}} + + test_path = base_path + ['traffic-engineering', 'segment-list', segment_list, 'index', index_value] + for address_family in ['ipv4', 'ipv6']: + source_identifier = addresses[address_family]['source_identifier'] + destination_identifier = addresses[address_family]['destination_identifier'] + # Make testcase re-entrant for next for loop + self.cli_delete(test_path) + + self.cli_set(test_path + ['mpls', 'label', mpls_label]) + self.cli_set(test_path + ['nai', 'adjacency', address_family, 'source-identifier', source_identifier]) + self.cli_set(test_path + ['nai', 'adjacency', address_family, 'destination-identifier', destination_identifier]) + self.cli_commit() + + frrconfig = self.getFRRconfig(f'segment-routing', stop_section='^exit') + self.assertIn(f'segment-routing', frrconfig) + self.assertIn(f' traffic-eng', frrconfig) + self.assertIn(f' mpls-te on', frrconfig) + self.assertIn(f' segment-list {segment_list}', frrconfig) + self.assertIn(f' index {index_value} mpls label {mpls_label}', frrconfig) + self.assertIn(f' index {index_value} nai adjacency {source_identifier} {destination_identifier}', frrconfig) + + def test_srte_mpls_label_and_prefix(self): + # Add segment-list with an mpls value and prefix + mpls_label = '1500' + segment_list = 'smoketest-mpls-with-prefix-segment-list' + index_value = '0' + + prefixes = {'ipv4' : {'prefix' : '192.168.255.0/24'}, + 'ipv6' : {'prefix' : '2003::/120'}} + + test_path = base_path + ['traffic-engineering', 'segment-list', segment_list, 'index', index_value] + for address_family in ['ipv4', 'ipv6']: + for algorithm_type in ['spf', 'strict-spf']: + prefix = prefixes[address_family]['prefix'] + # Make testcase re-entrant for next for loop + self.cli_delete(test_path) + + self.cli_set(test_path + ['mpls', 'label', mpls_label]) + self.cli_set(test_path + ['nai', 'prefix', address_family, 'prefix-identifier', prefix, 'algorithm', algorithm_type]) + self.cli_commit() + frrconfig = self.getFRRconfig(f'segment-routing', stop_section='^exit') + self.assertIn(f'segment-routing', frrconfig) + self.assertIn(f' traffic-eng', frrconfig) + self.assertIn(f' mpls-te on', frrconfig) + self.assertIn(f' segment-list {segment_list}', frrconfig) + if algorithm_type == 'spf': + self.assertIn(f' index {index_value} mpls label {mpls_label}', frrconfig) + self.assertIn(f' index {index_value} nai prefix {prefix} algorithm 0', frrconfig) + elif algorithm_type == 'strict-spf': + self.assertIn(f' index {index_value} mpls label {mpls_label}', frrconfig) + self.assertIn(f' index {index_value} nai prefix {prefix} algorithm 1', frrconfig) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_protocols_static.py b/smoketest/scripts/cli/test_protocols_static.py index 79d6b3af4..892a46d7b 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-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,7 +19,6 @@ 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 @@ -75,6 +74,18 @@ routes = { 'blackhole' : {}, 'reject' : { 'distance' : '10', 'tag' : '200' }, }, + '100.67.0.0/16': { + 'interface': { + 'eth1': {'segments': '2001:db8:aaaa::700'}, + }, + }, + '100.68.0.0/16': { + 'next_hop': { + '192.0.2.100': { + 'segments': '2001:db8:aaaa::400/2002::400/2003::400/2004::400' + }, + }, + }, '2001:db8:100::/40' : { 'next_hop' : { '2001:db8::1' : { 'distance' : '10' }, @@ -171,8 +182,6 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): super(TestProtocolsStatic, cls).setUpClass() 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 @classmethod def tearDownClass(cls): @@ -185,11 +194,14 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): self.cli_delete(['vrf']) self.cli_commit() - v4route = self.getFRRconfig('ip route', end='') + v4route = self.getFRRconfig('ip route') self.assertFalse(v4route) - v6route = self.getFRRconfig('ipv6 route', end='') + v6route = self.getFRRconfig('ipv6 route') self.assertFalse(v6route) + # always forward to base class + super().tearDown() + def test_01_static(self): self.cli_set(['vrf', 'name', 'black', 'table', '43210']) for route, route_config in routes.items(): @@ -254,7 +266,7 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig('ip route', end='') + frrconfig = self.getFRRconfig('ip route', end_marker='') # Verify routes for route, route_config in routes.items(): @@ -368,7 +380,7 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig('ip route', end='') + frrconfig = self.getFRRconfig('ip route', end_marker='') for table in tables: # Verify routes @@ -485,7 +497,7 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): self.assertEqual(tmp['linkinfo']['info_kind'], 'vrf') # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') + frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf') self.assertIn(f'vrf {vrf}', frrconfig) # Verify routes @@ -558,7 +570,7 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR configuration - frrconfig = self.getFRRconfig('ip mroute', end='') + frrconfig = self.getFRRconfig('ip mroute', end_marker='') 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(): @@ -581,7 +593,7 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): self.assertIn(tmp, frrconfig) def test_05_dhcp_default_route(self): - # When running via vyos-build under the QEmu environment a local DHCP + # 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'): @@ -597,7 +609,7 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): sleep(5) router = get_dhcp_router(interface) - frrconfig = self.getFRRconfig('') + 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" @@ -609,7 +621,7 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Re-check FRR configuration that default route is still present - frrconfig = self.getFRRconfig('') + 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']) @@ -619,5 +631,156 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): while process_named_running('dhclient', cmdline=interface, timeout=10): sleep(0.250) + def test_06_dhcp_default_route_for_vrf(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' + vrf = 'red' + vrf_path = ['vrf', 'name', vrf] + interface_path = ['interfaces', 'ethernet', interface] + self.cli_set(vrf_path + ['table', '1000']) + default_distance = default_value(interface_path + ['dhcp-options', 'default-route-distance']) + self.cli_set(interface_path + ['address', 'dhcp']) + self.cli_set(interface_path + ['vrf', vrf]) + self.cli_commit() + + router = get_dhcp_router(interface) + route_str = ( + rf'ip route 0.0.0.0/0 {router} {interface} tag 210 {default_distance}' + ) + + def check_default_route(): + frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf') + if route_str in frrconfig: + return True + return frrconfig + + result, config = self.wait_for_result( + check_default_route, True, pause=1, timeout=10 + ) + + # First clean interfaces from VRF so that VRF can be deleted + self.cli_delete(interface_path + ['address']) + self.cli_delete(interface_path + ['vrf']) + self.cli_commit() + + # now we can assert + self.assertTrue( + result, + f"Expected '{route_str}' in FRR config, vrf section of FRR config:\n {config}", + ) + + # Wait for dhclient to stop + while process_named_running('dhclient', cmdline=interface, timeout=10): + sleep(0.250) + + def test_07_dhcp_interface_static_routes(self): + # Test static routes using dhcp-interface option + # When running via vyos-build under the QEMU environment a local DHCP + # server is available. This test verifies that static routes with + # dhcp-interface are configured correctly. + if not os.path.exists('/tmp/vyos.smoketests.hint'): + self.skipTest('Not running under VyOS CI/CD QEMU environment!') + + dhcp_interface = 'eth0' + interface_path = ['interfaces', 'ethernet', dhcp_interface] + + # Configure DHCP on the interface + self.cli_set(interface_path + ['address', 'dhcp']) + + # Commit configuration + self.cli_commit() + + # Wait for dhclient to receive IP address + sleep(5) + + # Configure static routes with dhcp-interface + dhcp_routes = { + '10.10.0.0/16': { + 'dhcp_interface': [dhcp_interface], + }, + '192.168.100.0/24': { + 'dhcp_interface': [dhcp_interface], + }, + } + + # Configure the static routes + for route, route_config in dhcp_routes.items(): + base = base_path + ['route', route] + if 'dhcp_interface' in route_config: + for dhcp_if in route_config['dhcp_interface']: + self.cli_set(base + ['dhcp-interface', dhcp_if]) + + # Commit configuration + self.cli_commit() + + # Verify that the DHCP hook interface list file is created + dhcp_hook_iflist = '/tmp/static_dhcp_interfaces' + self.assertTrue( + os.path.exists(dhcp_hook_iflist), + 'DHCP hook interface list file should be created', + ) + + # Read the interface list file and verify it contains our interface + with open(dhcp_hook_iflist, 'r') as f: + interface_list = f.read().strip() + self.assertIn( + dhcp_interface, + interface_list, + f'Interface {dhcp_interface} should be in hook interface list', + ) + + # Get the DHCP router for verification + router = get_dhcp_router(dhcp_interface) + self.assertIsNotNone(router, 'DHCP router should be available') + + # Verify FRR configuration contains the static routes with DHCP router + frrconfig = self.getFRRconfig('ip route', end_marker='') + + for route in dhcp_routes.keys(): + expected_route = f'ip route {route} {router} {dhcp_interface}' + self.assertIn(expected_route, frrconfig, f'Static route {route} '\ + 'with dhcp-interface should be in FRR config') + + # Test table-based routes with dhcp-interface + table_id = '100' + table_route = '10.20.0.0/16' + table_base = base_path + ['table', table_id, 'route', table_route] + self.cli_set(table_base + ['dhcp-interface', dhcp_interface]) + self.cli_commit() + + # Verify table route in FRR config + frrconfig = self.getFRRconfig('ip route', end_marker='') + expected_table_route = ( + f'ip route {table_route} {router} {dhcp_interface} table {table_id}' + ) + self.assertIn( + expected_table_route, + frrconfig, + f'Table static route {table_route} with dhcp-interface should be in FRR config', + ) + + # Clean up - remove DHCP configuration + self.cli_delete(interface_path + ['address']) + self.cli_commit() + + # Wait for dhclient to stop + while process_named_running('dhclient', cmdline=dhcp_interface, timeout=10): + sleep(0.250) + + # Verify that the hook interface list file is cleaned up when no dhcp-interface routes exist + self.cli_delete(base_path) + self.cli_commit() + + # The interface list file should be removed when no dhcp-interface routes are configured + self.assertFalse( + os.path.exists(dhcp_hook_iflist), + 'DHCP hook interface list file should be removed when no dhcp-interface routes exist', + ) + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_protocols_static_arp.py b/smoketest/scripts/cli/test_protocols_static_arp.py index 7f8047249..447e5779d 100755 --- a/smoketest/scripts/cli/test_protocols_static_arp.py +++ b/smoketest/scripts/cli/test_protocols_static_arp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -49,6 +49,8 @@ class TestARP(VyOSUnitTestSHIM.TestCase): # delete test config self.cli_delete(base_path) self.cli_commit() + # always forward to base class + super().tearDown() def test_static_arp(self): test_data = { @@ -85,4 +87,4 @@ class TestARP(VyOSUnitTestSHIM.TestCase): self.assertTrue(found) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_protocols_traffic-engineering.py b/smoketest/scripts/cli/test_protocols_traffic-engineering.py new file mode 100755 index 000000000..e1e9d8475 --- /dev/null +++ b/smoketest/scripts/cli/test_protocols_traffic-engineering.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import unittest + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSessionError +from vyos.frrender import zebra_daemon +from vyos.utils.process import process_named_running + +base_path = ['protocols', 'traffic-engineering'] + +dummy_if1 = 'dum2191' +dummy_if2 = 'dum2192' + + +class TestProtocolsTrafficEngineering(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + # call base-classes classmethod + super(TestProtocolsTrafficEngineering, cls).setUpClass() + # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same + 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) + + cls.cli_set(cls, ['interfaces', 'dummy', dummy_if1]) + cls.cli_set(cls, ['interfaces', 'dummy', dummy_if2]) + cls.cli_commit(cls) + + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, ['interfaces', 'dummy', dummy_if2]) + cls.cli_delete(cls, ['interfaces', 'dummy', dummy_if1]) + cls.cli_commit(cls) + + super(TestProtocolsTrafficEngineering, cls).tearDownClass() + + def tearDown(self): + self.cli_delete(base_path) + self.cli_commit() + + # check process health and continuity + self.assertEqual(self.daemon_pid, process_named_running(zebra_daemon)) + # always forward to base class + super().tearDown() + + def test_te_normal(self): + self.cli_set(base_path + ['admin-group', 'cyan', 'bit-position', '1']) + self.cli_set(base_path + ['admin-group', 'magenta', 'bit-position', '3']) + + self.cli_set(base_path + ['interface', dummy_if1, 'admin-group', 'magenta']) + self.cli_set(base_path + ['interface', dummy_if1, 'max-bandwidth', '1024']) + self.cli_set( + base_path + ['interface', dummy_if1, 'max-reservable-bandwidth', '2048'] + ) + self.cli_set(base_path + ['interface', dummy_if1, 'metric', '74837']) + + self.cli_set(base_path + ['interface', dummy_if2, 'admin-group', 'cyan']) + self.cli_set(base_path + ['interface', dummy_if2, 'admin-group', 'magenta']) + + self.cli_commit() + + frrconfig = self.getFRRconfig(f'^interface {dummy_if1}', stop_section='^exit') + self.assertIn('link-params', frrconfig) + self.assertIn('metric 74837', frrconfig) + self.assertIn('admin-grp 0x8', frrconfig) + self.assertIn('max-bw 1.34218e+08', frrconfig) + self.assertIn('max-rsv-bw 2.68435e+08', frrconfig) + self.assertIn('unrsv-bw 0 2.68435e+08', frrconfig) + self.assertIn('unrsv-bw 1 2.68435e+08', frrconfig) + self.assertIn('unrsv-bw 2 2.68435e+08', frrconfig) + self.assertIn('unrsv-bw 3 2.68435e+08', frrconfig) + self.assertIn('unrsv-bw 4 2.68435e+08', frrconfig) + self.assertIn('unrsv-bw 5 2.68435e+08', frrconfig) + self.assertIn('unrsv-bw 6 2.68435e+08', frrconfig) + self.assertIn('unrsv-bw 7 2.68435e+08', frrconfig) + + frrconfig = self.getFRRconfig(f'^interface {dummy_if2}', stop_section='^exit') + self.assertIn('link-params', frrconfig) + self.assertIn('admin-grp 0xa', frrconfig) + + def test_te_verify(self): + self.cli_set(base_path + ['interface', dummy_if1, 'admin-group', 'cyan']) + + # Unknown group + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(base_path + ['admin-group', 'cyan', 'bit-position', '0']) + self.cli_set(base_path + ['admin-group', 'magenta', 'bit-position', '4']) + + # Now group is known + self.cli_commit() + + self.cli_set(base_path + ['admin-group', 'red', 'bit-position', '4']) + + # Same bit position as other group + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(base_path + ['admin-group', 'red', 'bit-position', '2']) + # Now should be ok + self.cli_commit() + + +if __name__ == '__main__': + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_qos.py b/smoketest/scripts/cli/test_qos.py index 231743344..8526e16b1 100755 --- a/smoketest/scripts/cli/test_qos.py +++ b/smoketest/scripts/cli/test_qos.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022-2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -86,6 +86,8 @@ class TestQoS(VyOSUnitTestSHIM.TestCase): # delete testing SSH config self.cli_delete(base_path) self.cli_commit() + # always forward to base class + super().tearDown() def test_01_cake(self): bandwidth = 1000000 @@ -242,7 +244,7 @@ class TestQoS(VyOSUnitTestSHIM.TestCase): self.assertEqual(flows, tmp['options']['flows']) self.assertEqual(queue_limit, tmp['options']['limit']) - # due to internal rounding we need to substract 1 from interval and target after converting to milliseconds + # due to internal rounding we need to subtract 1 from interval and target after converting to milliseconds # configuration of: # tc qdisc add dev eth0 root fq_codel quantum 1500 flows 512 interval 100ms limit 2048 target 5ms noecn # results in: tc -j qdisc show dev eth0 @@ -355,10 +357,10 @@ class TestQoS(VyOSUnitTestSHIM.TestCase): tc_details = get_tc_filter_details(interface, 'ingress') self.assertTrue('filter parent ffff: protocol all pref 20 u32 chain 0' in tc_details) - self.assertTrue('rate 1Gbit burst 15125b mtu 2Kb action drop overhead 0b linklayer ethernet' in tc_details) + self.assertTrue('rate 1Gbit burst 15Kb mtu 2Kb action drop overhead 0b linklayer ethernet' in tc_details) self.assertTrue('filter parent ffff: protocol all pref 15 u32 chain 0' in tc_details) - self.assertTrue('rate 3Gbit burst 102000b mtu 1600b action pipe/continue overhead 0b linklayer ethernet' in tc_details) - self.assertTrue('rate 500Mbit burst 204687b mtu 3000b action drop overhead 0b linklayer ethernet' in tc_details) + self.assertTrue('rate 3Gbit burst 100Kb mtu 1600b action pipe/continue overhead 0b linklayer ethernet' in tc_details) + self.assertTrue('rate 500Mbit burst 200Kb mtu 3000b action drop overhead 0b linklayer ethernet' in tc_details) self.assertTrue('filter parent ffff: protocol all pref 255 basic chain 0' in tc_details) def test_06_network_emulator(self): @@ -773,7 +775,7 @@ class TestQoS(VyOSUnitTestSHIM.TestCase): tc_filters = cmd(f'tc filter show dev {self._interfaces[0]} ingress') # class 100 self.assertIn('filter parent ffff: protocol all pref 20 fw chain 0', tc_filters) - self.assertIn('action order 1: police 0x1 rate 20Gbit burst 3847500b mtu 2Kb action drop overhead 0b', tc_filters) + self.assertIn('action order 1: police 0x1 rate 20Gbit burst 3760Kb 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) @@ -884,6 +886,8 @@ class TestQoS(VyOSUnitTestSHIM.TestCase): base_path + ['policy', 'cake', policy_name, 'bandwidth', str(bandwidth)] ) self.cli_set(base_path + ['policy', 'cake', policy_name, 'rtt', str(rtt)]) + self.cli_set(base_path + ['policy', 'cake', policy_name, 'no-split-gso']) + self.cli_set(base_path + ['policy', 'cake', policy_name, 'ack-filter', 'aggressive']) # commit changes self.cli_commit() @@ -899,6 +903,23 @@ class TestQoS(VyOSUnitTestSHIM.TestCase): self.assertFalse(tmp['options']['ingress']) self.assertFalse(tmp['options']['nat']) self.assertTrue(tmp['options']['raw']) + self.assertFalse(tmp['options']['split_gso']) + self.assertEqual(tmp['options']['ack-filter'], 'aggressive') + + self.cli_delete(base_path + ['policy', 'cake', policy_name, 'ack-filter', 'aggressive']) + self.cli_commit() + tmp = get_tc_qdisc_json(interface) + self.assertEqual(tmp['options']['ack-filter'], 'enabled') + + self.cli_delete(base_path + ['policy', 'cake', policy_name, 'ack-filter']) + self.cli_commit() + tmp = get_tc_qdisc_json(interface) + self.assertEqual(tmp['options']['ack-filter'], 'disabled') + + self.cli_delete(base_path + ['policy', 'cake', policy_name, 'no-split-gso']) + self.cli_commit() + tmp = get_tc_qdisc_json(interface) + self.assertTrue(tmp['options']['split_gso']) nat = True for flow_isolation in [ @@ -1232,7 +1253,7 @@ class TestQoS(VyOSUnitTestSHIM.TestCase): # 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) + self.assertIn('action order 1: police 0x1 rate 20Gbit burst 3760Kb 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) @@ -1305,4 +1326,4 @@ class TestQoS(VyOSUnitTestSHIM.TestCase): if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_broadcast-relay.py b/smoketest/scripts/cli/test_service_broadcast-relay.py index 87901869e..d51fd112c 100755 --- a/smoketest/scripts/cli/test_service_broadcast-relay.py +++ b/smoketest/scripts/cli/test_service_broadcast-relay.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2020 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -27,15 +27,26 @@ class TestServiceBroadcastRelay(VyOSUnitTestSHIM.TestCase): _address1 = '192.0.2.1/24' _address2 = '192.0.2.1/24' - def setUp(self): - self.cli_set(['interfaces', 'dummy', 'dum1001', 'address', self._address1]) - self.cli_set(['interfaces', 'dummy', 'dum1002', 'address', self._address2]) + @classmethod + def setUpClass(cls): + # always forward to base class + super(TestServiceBroadcastRelay, cls).setUpClass() - def tearDown(self): - self.cli_delete(['interfaces', 'dummy', 'dum1001']) - self.cli_delete(['interfaces', 'dummy', 'dum1002']) - self.cli_delete(base_path) - self.cli_commit() + cls.cli_set(cls, ['interfaces', 'dummy', 'dum1001', 'address', cls._address1]) + cls.cli_set(cls, ['interfaces', 'dummy', 'dum1002', 'address', cls._address2]) + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, ['interfaces', 'dummy', 'dum1001']) + cls.cli_delete(cls, ['interfaces', 'dummy', 'dum1002']) + cls.cli_delete(cls, base_path) + cls.cli_commit(cls) + + # always forward to base class + super(TestServiceBroadcastRelay, cls).tearDownClass() def test_broadcast_relay_service(self): ids = range(1, 5) @@ -65,4 +76,4 @@ class TestServiceBroadcastRelay(VyOSUnitTestSHIM.TestCase): self.assertTrue(running) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_config-sync.py b/smoketest/scripts/cli/test_service_config-sync.py new file mode 100644 index 000000000..926c08435 --- /dev/null +++ b/smoketest/scripts/cli/test_service_config-sync.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +# +# Copyright (C) VyOS Inc. +# +# 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 time +import unittest + +from base_vyostest_shim import VyOSUnitTestSHIM + +HTTPS_PATH = ['service', 'https'] +SYNC_PATH = ['service', 'config-sync'] + +ADDRESS = '127.0.0.1' +KEY = 'id_key' + + +class TestConfigSyncWithHTTPS(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.cli_delete(cls, HTTPS_PATH) + cls.cli_delete(cls, SYNC_PATH) + + def tearDown(self): + self.cli_delete(HTTPS_PATH) + self.cli_delete(SYNC_PATH) + + self.cli_delete(['interfaces', 'dummy']) + self.cli_delete(['system', 'time-zone']) + + self.cli_commit() + super().tearDown() + + def _configure_r1_config_sync(self): + """ + Simulates R1: config-sync client + """ + self.cli_set(SYNC_PATH + ['mode', 'load']) + self.cli_set(SYNC_PATH + ['secondary', 'address', ADDRESS]) + self.cli_set(SYNC_PATH + ['secondary', 'key', KEY]) + self.cli_set(SYNC_PATH + ['secondary', 'port', '443']) + + self.cli_set(SYNC_PATH + ['section', 'interfaces', 'dummy']) + self.cli_set(SYNC_PATH + ['section', 'system', 'time-zone']) + + self.cli_commit() + + # wait to init config-sync service + time.sleep(1) + + def _configure_r2_https_api(self): + """ + Simulates R2: HTTPS API endpoint + """ + self.cli_set(HTTPS_PATH + ['api', 'rest']) + self.cli_set(HTTPS_PATH + ['api', 'keys', 'id', 'KEY', 'key', KEY]) + self.cli_set(HTTPS_PATH + ['listen-address', '0.0.0.0']) + self.cli_commit() + + def test_basic(self): + """ + Validate: basic config-sync configuration (R1 side) + """ + + self._configure_r2_https_api() + self._configure_r1_config_sync() + + config = self.op_mode(['show', 'configuration', 'commands']) + + self.assertIn("set service config-sync mode 'load'", config) + self.assertIn(f"set service config-sync secondary address '{ADDRESS}'", config) + + def test_show_diff_candidate_interfaces(self): + """ + Validate: show configuration secondary sync commands candidate interfaces dummy + """ + + self._configure_r2_https_api() + self._configure_r1_config_sync() + + # committed config + self.cli_set(['interfaces', 'dummy', 'dum0', 'address', '192.0.2.1/32']) + self.cli_commit() + + # candidate change + self.cli_set(['interfaces', 'dummy', 'dum0', 'address', '192.0.2.2/32']) + + output = self.op_mode( + [ + 'show', + 'configuration', + 'secondary', + 'sync', + 'commands', + 'candidate', + 'interfaces', + 'dummy', + ] + ) + + self.assertIsInstance(output, str) + self.assertIn("set interfaces dummy dum0 address '192.0.2.2/32'", output) + + def test_show_diff_saved_system(self): + """ + Validate: show configuration secondary sync saved system time-zone + """ + + self._configure_r2_https_api() + self._configure_r1_config_sync() + + # committed config + self.cli_set(['system', 'time-zone', 'UTC']) + self.cli_commit() + + time.sleep(2) + + output = self.op_mode( + [ + 'show', + 'configuration', + 'secondary', + 'sync', + 'saved', + 'system', + 'time-zone', + ] + ) + + self.assertIsInstance(output, str) + self.assertIn('[system]\n- time-zone', output) + + output = self.op_mode(['show', 'configuration', 'secondary', 'sync', 'saved']) + + self.assertIsInstance(output, str) + self.assertIn('[system]\n- time-zone', output) + + def test_show_diff_empty(self): + """ + No candidate changes -> empty diff + """ + + self._configure_r2_https_api() + self._configure_r1_config_sync() + + output = self.op_mode(['show', 'configuration', 'secondary', 'sync']) + self.assertTrue(output.strip() == '' or 'No changes' in output, repr(output)) + + output = self.op_mode( + [ + 'show', + 'configuration', + 'secondary', + 'sync', + 'running', + 'interfaces', + 'dummy', + ] + ) + self.assertTrue(output.strip() == '' or 'No changes' in output) + + +if __name__ == '__main__': + unittest.main(verbosity=5) diff --git a/smoketest/scripts/cli/test_service_dhcp-relay.py b/smoketest/scripts/cli/test_service_dhcp-relay.py index 59c4b59a9..eca7c1c51 100755 --- a/smoketest/scripts/cli/test_service_dhcp-relay.py +++ b/smoketest/scripts/cli/test_service_dhcp-relay.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -31,6 +31,8 @@ class TestServiceDHCPRelay(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.cli_delete(base_path) self.cli_commit() + # always forward to base class + super().tearDown() def test_relay_default(self): max_size = '800' @@ -120,5 +122,4 @@ class TestServiceDHCPRelay(VyOSUnitTestSHIM.TestCase): self.assertTrue(process_named_running(PROCESS_NAME)) if __name__ == '__main__': - unittest.main(verbosity=2) - + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py index 7c2ebff89..a4def234c 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-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -32,9 +32,10 @@ from vyos.template import inc_ip from vyos.template import dec_ip PROCESS_NAME = 'kea-dhcp4' -CTRL_PROCESS_NAME = 'kea-ctrl-agent' -KEA4_CONF = '/run/kea/kea-dhcp4.conf' -KEA4_CTRL = '/run/kea/dhcp4-ctrl-socket' +D2_PROCESS_NAME = 'kea-dhcp-ddns' +KEA4_CONF = '/var/run/kea/kea-dhcp4.conf' +KEA4_D2_CONF = '/var/run/kea/kea-dhcp-ddns.conf' +KEA4_CTRL = '/var/run/kea/dhcp4-ctrl-socket' HOSTSD_CLIENT = '/usr/bin/vyos-hostsd-client' base_path = ['service', 'dhcp-server'] interface = 'dum8765' @@ -65,6 +66,8 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.cli_delete(base_path) self.cli_commit() + # always forward to base class + super().tearDown() def walk_path(self, obj, path): current = obj @@ -96,6 +99,15 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.assertTrue(key in base_obj) self.assertEqual(base_obj[key], value) + def verify_service_running(self): + try: + tmp = cmd('grep -i kea /var/log/messages | tail -n 100') + except OSError: + tmp = 'No relevant log entries' + 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' @@ -104,24 +116,9 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): range_1_start = inc_ip(subnet, 40) range_1_stop = inc_ip(subnet, 50) - self.cli_set(base_path + ['listen-interface', interface]) - - 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']) - # 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]) - self.cli_set(pool + ['option', 'name-server', dns_2]) - self.cli_set(pool + ['option', 'domain-name', domain_name]) - - # check validate() - No DHCP address range or active static-mapping set - with self.assertRaises(ConfigSessionError): - self.cli_commit() - self.cli_set(pool + ['range', '0', 'start', range_0_start]) - self.cli_set(pool + ['range', '0', 'stop', range_0_stop]) - self.cli_set(pool + ['range', '1', 'start', range_1_start]) - self.cli_set(pool + ['range', '1', 'stop', range_1_stop]) + self.setup_single_pool_range( + range_0_start, range_0_stop, range_1_start, range_1_stop, shared_net_name + ) # commit changes self.cli_commit() @@ -151,6 +148,21 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): 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, @@ -181,7 +193,215 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() + + def setup_single_pool_range( + self, range_0_start, range_0_stop, range_1_start, range_1_stop, shared_net_name + ): + 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]) + self.cli_set(pool + ['option', 'name-server', dns_2]) + self.cli_set(pool + ['option', 'domain-name', domain_name]) + + # check validate() - No DHCP address range or active static-mapping set + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(pool + ['range', '0', 'start', range_0_start]) + self.cli_set(pool + ['range', '0', 'stop', range_0_stop]) + self.cli_set(pool + ['range', '1', 'start', range_1_start]) + self.cli_set(pool + ['range', '1', 'stop', range_1_stop]) + + def test_dhcp_client_class(self): + shared_net_name = 'SMOKE-1' + + range_0_start = inc_ip(subnet, 10) + range_0_stop = inc_ip(subnet, 20) + range_1_start = inc_ip(subnet, 40) + range_1_stop = inc_ip(subnet, 50) + + self.setup_single_pool_range( + range_0_start, range_0_stop, range_1_start, range_1_stop, shared_net_name + ) + + self.cli_set( + base_path + + [ + 'shared-network-name', + shared_net_name, + 'subnet', + subnet, + 'client-class', + 'test', + ] + ) + + # check validate() - Client class referenced that doesn't exist yet + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete( + base_path + + [ + 'shared-network-name', + shared_net_name, + 'subnet', + subnet, + 'client-class', + 'test', + ] + ) + + self.cli_set( + base_path + + [ + 'shared-network-name', + shared_net_name, + 'subnet', + subnet, + 'range', + '0', + 'client-class', + 'test', + ] + ) + + # check validate() - Client class referenced that doesn't exist yet + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + base_path + + [ + 'shared-network-name', + shared_net_name, + 'subnet', + subnet, + 'client-class', + 'test', + ] + ) + + client_class = base_path + ['client-class', 'test'] + + # Test that invalid hex is rejected + self.cli_set( + client_class + ['relay-agent-information', 'circuit-id', '0xHELLOWORLD'] + ) + + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete(client_class + ['relay-agent-information', 'circuit-id']) + self.cli_set( + client_class + ['relay-agent-information', 'remote-id', '0xHELLOWORLD'] + ) + + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete(client_class + ['relay-agent-information', 'remote-id']) + + # Test string literals + self.cli_set(client_class + ['relay-agent-information', 'circuit-id', 'foo']) + self.cli_set(client_class + ['relay-agent-information', 'remote-id', 'bar']) + + self.cli_commit() + + self.check_client_class_in_config() + + self.cli_delete(client_class + ['relay-agent-information', 'circuit-id']) + self.cli_delete(client_class + ['relay-agent-information', 'remote-id']) + + # Test hex strings + self.cli_set( + client_class + ['relay-agent-information', 'circuit-id', '0x666f6f'] + ) + self.cli_set( + client_class + ['relay-agent-information', 'remote-id', '0x626172'] + ) + + self.cli_commit() + + self.check_client_class_in_config() + + def check_client_class_in_config(self): + config = read_file(KEA4_CONF) + obj = loads(config) + self.verify_config_value(obj, ['Dhcp4', 'client-classes', 0], 'name', 'test') + self.verify_config_value( + obj, + ['Dhcp4', 'client-classes', 0], + 'test', + 'relay4[1].hex == 0x666f6f and relay4[2].hex == 0x626172', + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0], 'client-class', 'test' + ) + self.verify_config_value( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0], + 'client-class', + 'test', + ) + # Check for running process + self.verify_service_running() + + def test_dhcp_vendor_option_ubiquiti(self): + shared_net_name = 'SMOKE-1' + + range_0_start = inc_ip(subnet, 10) + range_0_stop = inc_ip(subnet, 20) + range_1_start = inc_ip(subnet, 40) + range_1_stop = inc_ip(subnet, 50) + unifi_controller = '10.0.0.10' + + self.setup_single_pool_range( + range_0_start, range_0_stop, range_1_start, range_1_stop, shared_net_name + ) + + self.cli_set( + base_path + + [ + 'shared-network-name', + shared_net_name, + 'option', + 'vendor-option', + 'ubiquiti', + 'unifi-controller', + unifi_controller, + ] + ) + + self.cli_commit() + + config = read_file(KEA4_CONF) + obj = loads(config) + + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'option-data'], + {'name': 'vendor-encapsulated-options'}, + ) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'option-data'], + { + 'name': 'ubnt', + 'space': 'vendor-encapsulated-options-space', + 'data': unifi_controller, + }, + ) + self.verify_service_running() def test_dhcp_single_pool_options(self): shared_net_name = 'SMOKE-0815' @@ -197,6 +417,8 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): wpad = 'http://wpad.vyos.io/foo/bar' server_identifier = bootfile_server ipv6_only_preferred = '300' + capwap_access_controller = '192.168.2.125' + interface_mtu = '1420' pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] self.cli_set(pool + ['subnet-id', '1']) @@ -205,6 +427,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.cli_set(pool + ['option', 'name-server', dns_1]) self.cli_set(pool + ['option', 'name-server', dns_2]) self.cli_set(pool + ['option', 'domain-name', domain_name]) + self.cli_set(pool + ['option', 'interface-mtu', interface_mtu]) self.cli_set(pool + ['option', 'ip-forwarding']) self.cli_set(pool + ['option', 'smtp-server', smtp_server]) self.cli_set(pool + ['option', 'pop-server', smtp_server]) @@ -216,9 +439,14 @@ 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'] + 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']) @@ -261,6 +489,11 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.verify_config_object( obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'boot-file-name', 'data': bootfile_name}, + ) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], {'name': 'domain-name', 'data': domain_name}, ) self.verify_config_object( @@ -301,6 +534,11 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.verify_config_object( 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': 'tftp-server-name', 'data': tftp_server}, ) self.verify_config_object( @@ -312,31 +550,31 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): 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', + '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': 'windows-static-route', 'data': '24,10,0,0,192,0,2,1'}, + {'name': 'v6-only-preferred', 'data': ipv6_only_preferred}, ) self.verify_config_object( obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'v6-only-preferred', 'data': ipv6_only_preferred}, + {'name': 'ip-forwarding', 'data': 'true'}, ) self.verify_config_object( obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'ip-forwarding', 'data': 'true'}, + {'name': 'interface-mtu', 'data': interface_mtu}, ) # 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'}, + {'name': 'pcode', 'data': 'GMT0BST\\,M3.5.0/1\\,M10.5.0'}, ) self.verify_config_object( obj, @@ -352,7 +590,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ) # 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' @@ -438,7 +676,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ) # 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' @@ -531,6 +769,9 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.cli_delete(pool + ['static-mapping', 'dupe3']) self.cli_delete(pool + ['static-mapping', 'dupe4']) + # Create blank static mapping, will not be present in resulting config + self.cli_set(pool + ['static-mapping', 'blank']) + # commit changes self.cli_commit() @@ -584,7 +825,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): 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' @@ -726,7 +967,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): 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 @@ -773,7 +1014,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ) # 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 @@ -781,7 +1022,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): range_0_start = inc_ip(subnet, 10) range_0_stop = inc_ip(subnet, 100) - # the DHCP exclude addresse is blanked out of the range which is done + # the DHCP exclude address is blanked out of the range which is done # by slicing one range into two ranges exclude_addr = inc_ip(range_0_start, 20) range_0_stop_excl = dec_ip(exclude_addr, 1) @@ -836,7 +1077,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ) # 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 @@ -884,7 +1125,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ) # 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' @@ -987,8 +1228,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ) # 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' @@ -1087,8 +1327,294 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ) # 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 keys and domains configuration in the D2 config + self.verify_config_object( + 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==', + }, + ) + + 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( + 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']) @@ -1208,7 +1734,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): def internal_cleanup(): for seq in client_range: ip_addr = inc_ip(subnet, seq) - kea_delete_lease(4, ip_addr) + kea_delete_lease(4, None, ip_addr) cmd( f'{HOSTSD_CLIENT} --delete-hosts --tag dhcp-server-{ip_addr} --apply' ) @@ -1250,7 +1776,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() # All up and running, now test vyos-hostsd store @@ -1259,7 +1785,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): 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) + 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]}') @@ -1274,6 +1800,39 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): host_json = cmd(f'{HOSTSD_CLIENT} --get-hosts {tag_regex}') self.assertTrue(host_json) + def test_dhcp_log_level(self): + shared_net_name = 'SMOKE-TEST' + subnet_range_start = inc_ip(subnet, 10) + subnet_range_stop = inc_ip(subnet, 20) + log_level = 'error' + + 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]) + + # Set log level + self.cli_set(base_path + ['log-level', log_level]) + self.cli_commit() + + config = read_file(KEA4_CONF) + obj = loads(config) + + # Check log level is ERROR + self.verify_config_value(obj, ['Dhcp4', 'loggers', 0], 'name', 'kea-dhcp4') + self.verify_config_value( + obj, ['Dhcp4', 'loggers', 0], 'severity', log_level.upper() + ) + + # Delete log-level and check it is set to default INFO + self.cli_delete(base_path + ['log-level']) + self.cli_commit() + + config = read_file(KEA4_CONF) + obj = loads(config) + self.verify_config_value(obj, ['Dhcp4', 'loggers', 0], 'severity', 'INFO') + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_dhcpv6-relay.py b/smoketest/scripts/cli/test_service_dhcpv6-relay.py index e634a011f..71b318ae8 100755 --- a/smoketest/scripts/cli/test_service_dhcpv6-relay.py +++ b/smoketest/scripts/cli/test_service_dhcpv6-relay.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -107,4 +107,4 @@ class TestServiceDHCPv6Relay(VyOSUnitTestSHIM.TestCase): self.assertTrue(process_named_running(PROCESS_NAME)) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_dhcpv6-server.py b/smoketest/scripts/cli/test_service_dhcpv6-server.py index 6ecf6c1cf..04aa014b0 100755 --- a/smoketest/scripts/cli/test_service_dhcpv6-server.py +++ b/smoketest/scripts/cli/test_service_dhcpv6-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -26,7 +26,7 @@ from vyos.utils.process import process_named_running from vyos.utils.file import read_file PROCESS_NAME = 'kea-dhcp6' -KEA6_CONF = '/run/kea/kea-dhcp6.conf' +KEA6_CONF = '/var/run/kea/kea-dhcp6.conf' base_path = ['service', 'dhcpv6-server'] subnet = '2001:db8:f00::/64' @@ -37,18 +37,22 @@ nis_servers = ['2001:db8:ffff::1', '2001:db8:ffff::2'] interface = 'eth0' interface_addr = inc_ip(subnet, 1) + '/64' + class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestServiceDHCPv6Server, cls).setUpClass() # Clear out current configuration to allow running this test on a live system cls.cli_delete(cls, base_path) - - cls.cli_set(cls, ['interfaces', 'ethernet', interface, 'address', interface_addr]) + cls.cli_set( + cls, ['interfaces', 'ethernet', interface, 'address', interface_addr] + ) @classmethod def tearDownClass(cls): - cls.cli_delete(cls, ['interfaces', 'ethernet', interface, 'address', interface_addr]) + cls.cli_delete( + cls, ['interfaces', 'ethernet', interface, 'address', interface_addr] + ) cls.cli_commit(cls) super(TestServiceDHCPv6Server, cls).tearDownClass() @@ -56,6 +60,8 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.cli_delete(base_path) self.cli_commit() + # always forward to base class + super().tearDown() def walk_path(self, obj, path): current = obj @@ -68,7 +74,7 @@ class TestServiceDHCPv6Server(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] @@ -89,7 +95,7 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): def test_single_pool(self): shared_net_name = 'SMOKE-1' - search_domains = ['foo.vyos.net', 'bar.vyos.net'] + search_domains = ['foo.vyos.net', 'bar.vyos.net'] lease_time = '1200' max_lease_time = '72000' min_lease_time = '600' @@ -97,9 +103,11 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): sip_server = 'sip.vyos.net' sntp_server = inc_ip(subnet, 100) range_start = inc_ip(subnet, 256) # ::100 - range_stop = inc_ip(subnet, 65535) # ::ffff + range_stop = inc_ip(subnet, 65535) # ::ffff pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] + mapping = pool + ['static-mapping'] + conf_path = ['Dhcp6', 'shared-networks'] self.cli_set(base_path + ['preference', preference]) self.cli_set(pool + ['interface', interface]) @@ -108,6 +116,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]) @@ -118,6 +127,8 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): self.cli_set(pool + ['range', '1', 'start', range_start]) self.cli_set(pool + ['range', '1', 'stop', range_stop]) + self.cli_set(pool + ['option', 'time-zone', 'Europe/London']) + for server in nis_servers: self.cli_set(pool + ['option', 'nis-server', server]) self.cli_set(pool + ['option', 'nisplus-server', server]) @@ -125,19 +136,24 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): for search in search_domains: self.cli_set(pool + ['option', 'domain-search', search]) - client_base = 1 - for client in ['client1', 'client2', 'client3']: - duid = f'00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:{client_base:02}' - self.cli_set(pool + ['static-mapping', client, 'duid', duid]) - self.cli_set(pool + ['static-mapping', client, 'ipv6-address', inc_ip(subnet, client_base)]) - self.cli_set(pool + ['static-mapping', client, 'ipv6-prefix', inc_ip(subnet, client_base << 64) + '/64']) - client_base += 1 + for client_suffix in range(1, 4): + duid = f'00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:{client_suffix:02}' + ip1 = inc_ip(subnet, client_suffix * 2 - 1) + ip2 = inc_ip(subnet, client_suffix * 2) + prefix1 = inc_ip(subnet, (client_suffix * 2 - 1) << 64) + '/64' + prefix2 = inc_ip(subnet, (client_suffix * 2) << 64) + '/64' + + self.cli_set(mapping + [f'client{client_suffix}', 'duid', duid]) + self.cli_set(mapping + [f'client{client_suffix}', 'ipv6-address', ip1]) + self.cli_set(mapping + [f'client{client_suffix}', 'ipv6-address', ip2]) + self.cli_set(mapping + [f'client{client_suffix}', 'ipv6-prefix', prefix1]) + self.cli_set(mapping + [f'client{client_suffix}', 'ipv6-prefix', prefix2]) # cannot have both mac-address and duid set with self.assertRaises(ConfigSessionError): - self.cli_set(pool + ['static-mapping', 'client1', 'mac', '00:50:00:00:00:11']) + self.cli_set(mapping + ['client1', 'mac', '00:50:00:00:00:11']) self.cli_commit() - self.cli_delete(pool + ['static-mapping', 'client1', 'mac']) + self.cli_delete(mapping + ['client1', 'mac']) # commit changes self.cli_commit() @@ -145,114 +161,172 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): config = read_file(KEA6_CONF) obj = loads(config) - self.verify_config_value(obj, ['Dhcp6', 'shared-networks'], 'name', shared_net_name) - self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'subnet', subnet) - self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'interface', interface) - self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'id', 1) - self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'valid-lifetime', int(lease_time)) - self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'min-valid-lifetime', int(min_lease_time)) - self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'max-valid-lifetime', int(max_lease_time)) + self.verify_config_value(obj, conf_path, 'name', shared_net_name) + self.verify_config_value(obj, conf_path + [0, 'subnet6'], 'subnet', subnet) + self.verify_config_value( + obj, conf_path + [0, 'subnet6'], 'interface', interface + ) + self.verify_config_value(obj, conf_path + [0, 'subnet6'], 'id', 1) + self.verify_config_value( + obj, + conf_path + [0, 'subnet6'], + 'valid-lifetime', + int(lease_time), + ) + self.verify_config_value( + obj, + conf_path + [0, 'subnet6'], + 'min-valid-lifetime', + int(min_lease_time), + ) + self.verify_config_value( + obj, + conf_path + [0, 'subnet6'], + 'max-valid-lifetime', + int(max_lease_time), + ) # Verify options self.verify_config_object( - obj, - ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], - {'name': 'dns-servers', 'data': f'{dns_1}, {dns_2}'}) + obj, + conf_path + [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': 'domain-search', 'data': ", ".join(search_domains)}) + obj, + conf_path + [0, 'subnet6', 0, 'option-data'], + {'name': 'dns-servers', 'data': f'{dns_1}, {dns_2}'}, + ) self.verify_config_object( - obj, - ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], - {'name': 'nis-domain-name', 'data': domain}) + obj, + conf_path + [0, 'subnet6', 0, 'option-data'], + {'name': 'domain-search', 'data': ', '.join(search_domains)}, + ) self.verify_config_object( - obj, - ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], - {'name': 'nis-servers', 'data': ", ".join(nis_servers)}) + obj, + conf_path + [0, 'subnet6', 0, 'option-data'], + {'name': 'nis-domain-name', 'data': domain}, + ) self.verify_config_object( - obj, - ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], - {'name': 'nisp-domain-name', 'data': domain}) + obj, + conf_path + [0, 'subnet6', 0, 'option-data'], + {'name': 'nis-servers', 'data': ', '.join(nis_servers)}, + ) self.verify_config_object( - obj, - ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], - {'name': 'nisp-servers', 'data': ", ".join(nis_servers)}) + obj, + conf_path + [0, 'subnet6', 0, 'option-data'], + {'name': 'nisp-domain-name', 'data': domain}, + ) self.verify_config_object( - obj, - ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], - {'name': 'sntp-servers', 'data': sntp_server}) + obj, + conf_path + [0, 'subnet6', 0, 'option-data'], + {'name': 'nisp-servers', 'data': ', '.join(nis_servers)}, + ) self.verify_config_object( - obj, - ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], - {'name': 'sip-server-dns', 'data': sip_server}) + obj, + conf_path + [0, 'subnet6', 0, 'option-data'], + {'name': 'sntp-servers', 'data': sntp_server}, + ) + self.verify_config_object( + obj, + conf_path + [0, 'subnet6', 0, 'option-data'], + {'name': 'sip-server-dns', 'data': sip_server}, + ) - # Verify pools + # Time zone self.verify_config_object( - obj, - ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'pools'], - {'pool': f'{range_start} - {range_stop}'}) + obj, + conf_path + [0, 'subnet6', 0, 'option-data'], + {'name': 'new-posix-timezone', 'data': 'GMT0BST\\,M3.5.0/1\\,M10.5.0'}, + ) + self.verify_config_object( + obj, + conf_path + [0, 'subnet6', 0, 'option-data'], + {'name': 'new-tzdb-timezone', 'data': 'Europe/London'}, + ) - client_base = 1 - for client in ['client1', 'client2', 'client3']: - duid = f'00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:{client_base:02}' - ip = inc_ip(subnet, client_base) - prefix = inc_ip(subnet, client_base << 64) + '/64' + # Verify pools + self.verify_config_object( + obj, + conf_path + [0, 'subnet6', 0, 'pools'], + {'pool': f'{range_start} - {range_stop}'}, + ) + + for client_suffix in range(1, 4): + duid = f'00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:{client_suffix:02}' + ip1 = inc_ip(subnet, client_suffix * 2 - 1) + ip2 = inc_ip(subnet, client_suffix * 2) + prefix1 = inc_ip(subnet, (client_suffix * 2 - 1) << 64) + '/64' + prefix2 = inc_ip(subnet, (client_suffix * 2) << 64) + '/64' self.verify_config_object( - obj, - ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'reservations'], - {'hostname': client, 'duid': duid, 'ip-addresses': [ip], 'prefixes': [prefix]}) - - client_base += 1 + obj, + conf_path + [0, 'subnet6', 0, 'reservations'], + { + 'hostname': f'client{client_suffix}', + 'duid': duid, + 'ip-addresses': [ip1, ip2], + 'prefixes': [prefix1, prefix2], + }, + ) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) - def test_prefix_delegation(self): shared_net_name = 'SMOKE-2' range_start = inc_ip(subnet, 256) # ::100 - range_stop = inc_ip(subnet, 65535) # ::ffff + range_stop = inc_ip(subnet, 65535) # ::ffff delegate_start = '2001:db8:ee::' delegate_len = '64' + bad_prefix_len = '32' prefix_len = '56' exclude_len = '66' pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] + pd_mapping = pool + ['prefix-delegation'] + pd_mapping_prefix = pd_mapping + ['prefix', delegate_start] + conf_path = ['Dhcp6', 'shared-networks'] + self.cli_set(pool + ['subnet-id', '1']) self.cli_set(pool + ['range', '1', 'start', range_start]) self.cli_set(pool + ['range', '1', 'stop', range_stop]) - self.cli_set(pool + ['prefix-delegation', 'prefix', delegate_start, 'delegated-length', delegate_len]) - self.cli_set(pool + ['prefix-delegation', 'prefix', delegate_start, 'prefix-length', prefix_len]) - self.cli_set(pool + ['prefix-delegation', 'prefix', delegate_start, 'excluded-prefix', delegate_start]) - self.cli_set(pool + ['prefix-delegation', 'prefix', delegate_start, 'excluded-prefix-length', exclude_len]) + self.cli_set(pd_mapping_prefix + ['delegated-length', delegate_len]) + self.cli_set(pd_mapping_prefix + ['prefix-length', bad_prefix_len]) + self.cli_set(pd_mapping_prefix + ['excluded-prefix', delegate_start]) + self.cli_set(pd_mapping_prefix + ['excluded-prefix-length', exclude_len]) # commit changes + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(pd_mapping_prefix + ['prefix-length', prefix_len]) self.cli_commit() config = read_file(KEA6_CONF) obj = loads(config) - self.verify_config_value(obj, ['Dhcp6', 'shared-networks'], 'name', shared_net_name) - self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'subnet', subnet) + self.verify_config_value(obj, conf_path, 'name', shared_net_name) + self.verify_config_value(obj, conf_path + [0, 'subnet6'], 'subnet', subnet) # Verify pools self.verify_config_object( - obj, - ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'pools'], - {'pool': f'{range_start} - {range_stop}'}) + obj, + conf_path + [0, 'subnet6', 0, 'pools'], + {'pool': f'{range_start} - {range_stop}'}, + ) self.verify_config_object( - obj, - ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'pd-pools'], - { - 'prefix': delegate_start, - 'prefix-len': int(prefix_len), - 'delegated-len': int(delegate_len), - 'excluded-prefix': delegate_start, - 'excluded-prefix-len': int(exclude_len) - }) + obj, + conf_path + [0, 'subnet6', 0, 'pd-pools'], + { + 'prefix': delegate_start, + 'prefix-len': int(prefix_len), + 'delegated-len': int(delegate_len), + 'excluded-prefix': delegate_start, + 'excluded-prefix-len': int(exclude_len), + }, + ) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) @@ -262,9 +336,12 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): ns_global_1 = '2001:db8::1111' ns_global_2 = '2001:db8::2222' + pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] + conf_path = ['Dhcp6', 'shared-networks'] + self.cli_set(base_path + ['global-parameters', 'name-server', ns_global_1]) self.cli_set(base_path + ['global-parameters', 'name-server', ns_global_2]) - self.cli_set(base_path + ['shared-network-name', shared_net_name, 'subnet', subnet, 'subnet-id', '1']) + self.cli_set(pool + ['subnet-id', '1']) # commit changes self.cli_commit() @@ -272,17 +349,25 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): config = read_file(KEA6_CONF) obj = loads(config) - self.verify_config_value(obj, ['Dhcp6', 'shared-networks'], 'name', shared_net_name) - self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'subnet', subnet) - self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'id', 1) + self.verify_config_value(obj, conf_path, 'name', shared_net_name) + self.verify_config_value(obj, conf_path + [0, 'subnet6'], 'subnet', subnet) + self.verify_config_value(obj, conf_path + [0, 'subnet6'], 'id', 1) self.verify_config_object( - obj, - ['Dhcp6', 'option-data'], - {'name': 'dns-servers', "code": 23, "space": "dhcp6", "csv-format": True, 'data': f'{ns_global_1}, {ns_global_2}'}) + obj, + ['Dhcp6', 'option-data'], + { + 'name': 'dns-servers', + 'code': 23, + 'space': 'dhcp6', + 'csv-format': True, + 'data': f'{ns_global_1}, {ns_global_2}', + }, + ) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index 522102e67..c68b831af 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-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -59,6 +59,8 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # Check for process not running anymore self.assertFalse(process_named_running(DDCLIENT_PNAME)) + # always forward to base class + super().tearDown() # IPv4 standard DDNS service configuration def test_01_dyndns_service_standard(self): @@ -73,7 +75,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): for opt, value in details.items(): self.cli_set(name_path + [svc, opt, value]) - # 'zone' option is supported by 'cloudfare' and 'zoneedit1', but not 'freedns' + # 'zone' option is supported by 'cloudflare' and 'zoneedit1', but not 'freedns' self.cli_set(name_path + [svc, 'zone', zone]) if details['protocol'] in ['cloudflare', 'zoneedit1']: pass @@ -83,7 +85,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.cli_commit() self.cli_delete(name_path + [svc, 'zone']) - # 'ttl' option is supported by 'cloudfare', but not 'freedns' and 'zoneedit' + # 'ttl' option is supported by 'cloudflare', but not 'freedns' and 'zoneedit' self.cli_set(name_path + [svc, 'ttl', ttl]) if details['protocol'] == 'cloudflare': pass @@ -172,7 +174,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): for opt, value in details.items(): self.cli_set(name_path + [name, opt, value]) - # Dual stack is supported by 'cloudfare' and 'freedns' but not 'googledomains' + # Dual stack is supported by 'cloudflare' and 'freedns' but not 'googledomains' # exception is raised for unsupported ones self.cli_set(name_path + [name, 'ip-version', ip_version]) if details['protocol'] not in ['cloudflare', 'freedns']: @@ -360,4 +362,4 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.cli_delete(['vrf', 'name', vrf_name]) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_dns_forwarding.py b/smoketest/scripts/cli/test_service_dns_forwarding.py index 9a3f4933e..ffc0d7734 100755 --- a/smoketest/scripts/cli/test_service_dns_forwarding.py +++ b/smoketest/scripts/cli/test_service_dns_forwarding.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -49,6 +49,14 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): # out the current configuration :) cls.cli_delete(cls, base_path) + def setUp(self): + # always forward to base class + super().setUp() + for network in allow_from: + self.cli_set(base_path + ['allow-from', network]) + for address in listen_adress: + self.cli_set(base_path + ['listen-address', address]) + def tearDown(self): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) @@ -59,14 +67,8 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): # Check for running process self.assertFalse(process_named_running(PROCESS_NAME)) - - def setUp(self): - # forward to base class - super().setUp() - for network in allow_from: - self.cli_set(base_path + ['allow-from', network]) - for address in listen_adress: - self.cli_set(base_path + ['listen-address', address]) + # always forward to base class + super().tearDown() def test_basic_forwarding(self): # Check basic DNS forwarding settings @@ -341,4 +343,4 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py index 04c4a2e51..b29be9eda 100755 --- a/smoketest/scripts/cli/test_service_https.py +++ b/smoketest/scripts/cli/test_service_https.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,6 +16,8 @@ import unittest import json +import psutil +import time from requests import request from urllib3.exceptions import InsecureRequestWarning @@ -26,6 +28,7 @@ from base_vyostest_shim import ignore_warning from vyos.utils.file import read_file from vyos.utils.file import write_file from vyos.utils.process import call +from vyos.utils.process import cmd from vyos.utils.process import process_named_running from vyos.xml_ref import default_value @@ -34,6 +37,9 @@ from vyos.configsession import ConfigSessionError base_path = ['service', 'https'] pki_base = ['pki'] +address = '127.0.0.1' +key = 'VyOS-key' + cert_data = """ MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIw WTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNv @@ -112,6 +118,94 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): # Check for stopped process self.assertFalse(process_named_running(PROCESS_NAME)) + # always forward to base class + super().tearDown() + + def _api_get_background_operations(self): + url = f'https://{address}/retrieve/background-operations' + r = request('POST', url, verify=False, json={'key': key}) + self.assertEqual(r.status_code, 200) + body = r.json() + self.assertTrue(body.get('success')) + ops = body.get('data', {}).get('operations', []) + return ops + + def _wait_no_active_operations(self, timeout: int = 30): + # wait until no queued/running operations remain + deadline = time.time() + timeout + statuses = ('queued', 'running') + + while time.time() < deadline: + ops = self._api_get_background_operations() + ops = [op for op in ops if op.get('status') in statuses] + if not ops: + return + sleep(0.25) + self.fail('Timeout waiting for background operations to finish') + + def assertBackgroundOpResponseIsOk(self, response): + self.assertEqual(response.status_code, 200) + body = response.json() + self.assertTrue(body.get('success')) + + data = body.get('data', {}) + self.assertIsInstance(data, dict) + self.assertIn('operation', data) + op = data['operation'] + self.assertIn('op_id', op) + self.assertIn('status', op) + + 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_listen_address_vrf(self): + # Verify that HTTPS service can be configured with a listen-address + # inside a VRF. Regression test: the port availability check used to + # fail because it ran in the default namespace where the VRF address + # is unreachable. + vrf = 'mgmt' + vrf_table = '1337' + test_addr = '192.0.2.1' + test_prefix = f'{test_addr}/26' + interface = 'dum0' + + self.cli_set(['interfaces', 'dummy', interface, 'address', test_prefix]) + self.cli_set(['interfaces', 'dummy', interface, 'vrf', vrf]) + self.cli_set(['vrf', 'name', vrf, 'table', vrf_table]) + + self.cli_set( + base_path + ['api', 'keys', 'id', 'key-01', 'key', 'MySuperSecretVyOS'] + ) + self.cli_set(base_path + ['listen-address', test_addr]) + self.cli_set(base_path + ['vrf', vrf]) + self.cli_commit() + + # Verify nginx is running inside the VRF + tmp = cmd(f'ip vrf pids {vrf}') + self.assertIn(PROCESS_NAME, tmp) + + self.cli_delete(['interfaces', 'dummy', interface]) + self.cli_delete(['vrf', 'name', vrf]) def test_certificate(self): cert_name = 'test_https' @@ -330,8 +424,6 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): @ignore_warning(InsecureRequestWarning) def test_api_add_delete(self): - address = '127.0.0.1' - key = 'VyOS-key' url = f'https://{address}/retrieve' payload = {'data': '{"op": "showConfig", "path": []}', 'key': f'{key}'} headers = {} @@ -361,8 +453,6 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): @ignore_warning(InsecureRequestWarning) def test_api_show(self): - address = '127.0.0.1' - key = 'VyOS-key' url = f'https://{address}/show' headers = {} @@ -379,8 +469,6 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): @ignore_warning(InsecureRequestWarning) def test_api_generate(self): - address = '127.0.0.1' - key = 'VyOS-key' url = f'https://{address}/generate' headers = {} @@ -397,8 +485,6 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): @ignore_warning(InsecureRequestWarning) def test_api_configure(self): - address = '127.0.0.1' - key = 'VyOS-key' url = f'https://{address}/configure' headers = {} conf_interface = 'dum0' @@ -423,8 +509,6 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): @ignore_warning(InsecureRequestWarning) def test_api_config_file(self): - address = '127.0.0.1' - key = 'VyOS-key' url = f'https://{address}/config-file' headers = {} @@ -441,8 +525,6 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): @ignore_warning(InsecureRequestWarning) def test_api_reset(self): - address = '127.0.0.1' - key = 'VyOS-key' url = f'https://{address}/reset' headers = {} @@ -459,8 +541,6 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): @ignore_warning(InsecureRequestWarning) def test_api_image(self): - address = '127.0.0.1' - key = 'VyOS-key' url = f'https://{address}/image' headers = {} @@ -502,8 +582,6 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): @ignore_warning(InsecureRequestWarning) def test_api_config_file_load_http(self): # Test load config from HTTP URL - address = '127.0.0.1' - key = 'VyOS-key' url = f'https://{address}/config-file' url_config = f'https://{address}/configure' headers = {} @@ -546,6 +624,125 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): call(f'sudo rm -f {nginx_tmp_site}') call('sudo systemctl reload nginx') + @ignore_warning(InsecureRequestWarning) + def test_api_configure_background(self): + url = f'https://{address}/configure' + conf_interface = 'dum8' + conf_address = '192.0.2.88/32' + + # Enable REST API + 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', + conf_interface, + 'address', + ] + params = {'in_background': True} + payload = { + 'data': json.dumps( + {'op': 'set', 'path': payload_path, 'value': conf_address} + ), + 'key': key, + } + + r = request('POST', url, verify=False, params=params, data=payload) + self.assertBackgroundOpResponseIsOk(r) + body = r.json() + op = body.get('data', {}).get('operation', []) + + # Operation should appear as active shortly + ops = self._api_get_background_operations() + self.assertTrue( + any(o.get('op_id') == op.get('op_id') for o in ops), + 'Queued operation is not visible in `/retrieve/background-operations`', + ) + + # Wait until done + self._wait_no_active_operations() + + # Verify config applied (using CLI show) + self.assertIn(conf_address, self.op_mode(['show', 'configuration', 'commands'])) + + @ignore_warning(InsecureRequestWarning) + def test_api_configure_section_background(self): + url = f'https://{address}/configure-section' + conf_interface = 'dum9' + conf_address = '192.0.2.99/32' + + # Enable REST API + self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key]) + self.cli_set(base_path + ['api', 'rest']) + self.cli_commit() + + # Configure-section payload: set a full section + # example: set interfaces dummy dum8 address 192.0.2.99/32 + payload = { + 'data': json.dumps( + { + 'op': 'set', + 'path': ['interfaces', 'dummy', conf_interface], + 'section': { + 'address': [conf_address], + }, + } + ), + 'key': key, + } + params = {'in_background': True} + + r = request('POST', url, verify=False, params=params, data=payload) + self.assertBackgroundOpResponseIsOk(r) + + # Wait until done + self._wait_no_active_operations() + + # Verify section applied + self.assertIn(conf_address, self.op_mode(['show', 'configuration', 'commands'])) + + @ignore_warning(InsecureRequestWarning) + def test_api_configure_background_ops_over_max(self): + max_ops = 128 + + # Enable REST API + self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key]) + self.cli_set(base_path + ['api', 'rest']) + self.cli_commit() + + op_ids = [] + params = {'in_background': True} + url = f'https://{address}/configure' + + # Create many non-existent configurations to fill the queue. + for i in range(max_ops + 5): + config_name = f'invalid-test-option-{i}' + payload_path = ['system', config_name] + payload = { + 'data': json.dumps( + {'op': 'set', 'path': payload_path, 'value': config_name} + ), + 'key': key, + } + + with self.subTest(payload_path=payload_path): + r = request('POST', url, verify=False, params=params, data=payload) + self.assertBackgroundOpResponseIsOk(r) + + body = r.json() + op = body.get('data', {}).get('operation', []) + op_ids.append(op) + + # Wait for queue to drain + self._wait_no_active_operations(timeout=120) + + # Verify pruning: oldest `op_id` should be absent, and count should be <= `max_ops` + ops = self._api_get_background_operations() + self.assertLessEqual(len(ops), max_ops) + self.assertFalse(any(o.get('op_id') == op_ids[0] for o in ops)) + 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 3b3c205cd..614295fd8 100755 --- a/smoketest/scripts/cli/test_service_ipoe-server.py +++ b/smoketest/scripts/cli/test_service_ipoe-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,10 @@ import re import unittest -from collections import OrderedDict from base_accel_ppp_test import BasicAccelPPPTest +from base_vyostest_shim import VyOSUnitTestSHIM +from collections import OrderedDict + from vyos.configsession import ConfigSessionError from vyos.utils.process import cmd from vyos.template import range_to_regex @@ -317,6 +319,25 @@ delegate={delegate_2_prefix},{delegate_mask},name={pool_name}""" conf.read(self._config_file) self.assertIn(f'start={start_session}', conf['ipoe']['interface']) + def test_ipoe_server_idle_timeout(self): + idle_timeout = '300' + + self.basic_config() + self.cli_commit() + + # Default: no idle-timeout emitted + conf = ConfigParser(allow_no_value=True, delimiters='=', strict=False) + conf.read(self._config_file) + self.assertNotIn('idle-timeout', conf['ipoe']) + + # Configure idle-timeout + self.set(['idle-timeout', idle_timeout]) + self.cli_commit() + + conf = ConfigParser(allow_no_value=True, delimiters='=', strict=False) + conf.read(self._config_file) + self.assertEqual(conf['ipoe']['idle-timeout'], idle_timeout) + @unittest.skip("PPP is not a part of IPoE") def test_accel_ppp_options(self): pass @@ -326,4 +347,4 @@ delegate={delegate_2_prefix},{delegate_mask},name={pool_name}""" pass if __name__ == "__main__": - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_lldp.py b/smoketest/scripts/cli/test_service_lldp.py index c73707e0d..f01bc23d2 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-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -59,6 +59,8 @@ class TestServiceLLDP(VyOSUnitTestSHIM.TestCase): # service is no longer allowed to run after it was removed self.assertFalse(process_named_running(PROCESS_NAME)) + # always forward to base class + super().tearDown() def test_01_lldp_basic(self): self.cli_set(base_path) @@ -124,7 +126,7 @@ class TestServiceLLDP(VyOSUnitTestSHIM.TestCase): def test_06_lldp_snmp(self): self.cli_set(base_path + ['snmp']) - # verify - can not start lldp snmp without snmp beeing configured + # verify - can not start lldp snmp without snmp being configured with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(['service', 'snmp']) @@ -182,4 +184,4 @@ class TestServiceLLDP(VyOSUnitTestSHIM.TestCase): self.assertIn(f'configure ports {interface} lldp status rx-and-tx', config) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_mdns_repeater.py b/smoketest/scripts/cli/test_service_mdns_repeater.py index 30e48683f..ee46ed694 100755 --- a/smoketest/scripts/cli/test_service_mdns_repeater.py +++ b/smoketest/scripts/cli/test_service_mdns_repeater.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -26,6 +26,7 @@ from vyos.xml_ref import default_value base_path = ['service', 'mdns', 'repeater'] intf_base = ['interfaces', 'dummy'] config_file = '/run/avahi-daemon/avahi-daemon.conf' +PROCESS_NAME = 'avahi-daemon' class TestServiceMDNSrepeater(VyOSUnitTestSHIM.TestCase): @classmethod @@ -57,13 +58,13 @@ class TestServiceMDNSrepeater(VyOSUnitTestSHIM.TestCase): def tearDown(self): # Check for running process - self.assertTrue(process_named_running('avahi-daemon')) - + self.assertTrue(process_named_running(PROCESS_NAME)) self.cli_delete(base_path) self.cli_commit() - # Check that there is no longer a running process - self.assertFalse(process_named_running('avahi-daemon')) + self.assertFalse(process_named_running(PROCESS_NAME)) + # always forward to base class + super().tearDown() def test_service_dual_stack(self): # mDNS browsing domains in addition to the default one (local) @@ -101,7 +102,7 @@ class TestServiceMDNSrepeater(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['interface', 'dum10']) self.cli_set(base_path + ['interface', 'dum40']) - # exception is raised if partcipating interfaces do not have IPv4 address + # exception is raised if participating interfaces do not have IPv4 address with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(base_path + ['interface', 'dum40']) @@ -118,12 +119,12 @@ class TestServiceMDNSrepeater(VyOSUnitTestSHIM.TestCase): self.assertEqual(conf['reflector']['enable-reflector'], 'yes') def test_service_ipv6(self): - # partcipating interfaces should have IPv6 addresses + # participating interfaces should have IPv6 addresses self.cli_set(base_path + ['ip-version', 'ipv6']) self.cli_set(base_path + ['interface', 'dum10']) self.cli_set(base_path + ['interface', 'dum30']) - # exception is raised if partcipating interfaces do not have IPv4 address + # exception is raised if participating interfaces do not have IPv4 address with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(base_path + ['interface', 'dum10']) @@ -173,4 +174,4 @@ class TestServiceMDNSrepeater(VyOSUnitTestSHIM.TestCase): self.assertEqual(conf['server']['cache-entries-max'], cache_entries) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_monitoring_network_event.py b/smoketest/scripts/cli/test_service_monitoring_network_event.py index 3c9b4bf7f..e36e16b03 100644 --- a/smoketest/scripts/cli/test_service_monitoring_network_event.py +++ b/smoketest/scripts/cli/test_service_monitoring_network_event.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -38,6 +38,8 @@ class TestMonitoringNetworkEvent(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.cli_delete(base_path) self.cli_commit() + # always forward to base class + super().tearDown() def test_network_event_log(self): expected_config = { @@ -62,4 +64,4 @@ class TestMonitoringNetworkEvent(VyOSUnitTestSHIM.TestCase): if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_monitoring_prometheus.py b/smoketest/scripts/cli/test_service_monitoring_prometheus.py index 6e7f8c808..df27162ef 100755 --- a/smoketest/scripts/cli/test_service_monitoring_prometheus.py +++ b/smoketest/scripts/cli/test_service_monitoring_prometheus.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -53,6 +53,8 @@ class TestMonitoringPrometheus(VyOSUnitTestSHIM.TestCase): self.cli_commit() self.assertFalse(process_named_running(NODE_EXPORTER_PROCESS_NAME)) self.assertFalse(process_named_running(FRR_EXPORTER_PROCESS_NAME)) + # always forward to base class + super().tearDown() def test_01_node_exporter(self): self.cli_set(base_path + ['node-exporter', 'listen-address', listen_ip]) @@ -158,4 +160,4 @@ class TestMonitoringPrometheus(VyOSUnitTestSHIM.TestCase): if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_monitoring_telegraf.py b/smoketest/scripts/cli/test_service_monitoring_telegraf.py index 886b88683..3e401cc7e 100755 --- a/smoketest/scripts/cli/test_service_monitoring_telegraf.py +++ b/smoketest/scripts/cli/test_service_monitoring_telegraf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -36,12 +36,12 @@ class TestMonitoringTelegraf(VyOSUnitTestSHIM.TestCase): def tearDown(self): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) - self.cli_delete(base_path) self.cli_commit() - # Check for not longer running process self.assertFalse(process_named_running(PROCESS_NAME)) + # always forward to base class + super().tearDown() def test_01_basic_config(self): self.cli_set(base_path + ['influxdb', 'authentication', 'organization', org]) @@ -93,4 +93,4 @@ class TestMonitoringTelegraf(VyOSUnitTestSHIM.TestCase): if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_monitoring_zabbix-agent.py b/smoketest/scripts/cli/test_service_monitoring_zabbix-agent.py index 522f9df0f..a98999d9e 100755 --- a/smoketest/scripts/cli/test_service_monitoring_zabbix-agent.py +++ b/smoketest/scripts/cli/test_service_monitoring_zabbix-agent.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -31,12 +31,12 @@ class TestZabbixAgent(VyOSUnitTestSHIM.TestCase): def tearDown(self): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) - self.cli_delete(base_path) self.cli_commit() - # Process must be terminated after deleting the config self.assertFalse(process_named_running(PROCESS_NAME)) + # always forward to base class + super().tearDown() def test_01_zabbix_agent(self): directory = '/tmp' @@ -105,4 +105,4 @@ class TestZabbixAgent(VyOSUnitTestSHIM.TestCase): if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_ndp-proxy.py b/smoketest/scripts/cli/test_service_ndp-proxy.py index dfdb3f6aa..f89ea0fac 100755 --- a/smoketest/scripts/cli/test_service_ndp-proxy.py +++ b/smoketest/scripts/cli/test_service_ndp-proxy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,6 +18,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section from vyos.utils.process import cmd from vyos.utils.process import process_named_running @@ -43,12 +44,13 @@ class TestServiceNDPProxy(VyOSUnitTestSHIM.TestCase): def tearDown(self): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) - # delete testing SSH config self.cli_delete(base_path) self.cli_commit() self.assertFalse(process_named_running(PROCESS_NAME)) + # always forward to base class + super().tearDown() def test_basic(self): interfaces = Section.interfaces('ethernet') @@ -65,5 +67,63 @@ class TestServiceNDPProxy(VyOSUnitTestSHIM.TestCase): self.assertIn(f'timeout 500', config) # default value self.assertIn(f'ttl 30000', config) # default value + def test_prefix_mode_interface_requires_interface(self): + interface = Section.interfaces('ethernet')[0] + prefix_path = base_path + ['interface', interface, 'prefix', '2001:db8::/64'] + self.cli_set(base_path + ['interface', interface]) + self.cli_commit() + + self.cli_set(prefix_path + ['mode', 'interface']) + + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + def test_prefix_mode_interface_with_interface(self): + interface = Section.interfaces('ethernet')[0] + prefix_path = base_path + ['interface', interface, 'prefix', '2001:db8::/64'] + self.cli_set(base_path + ['interface', interface]) + self.cli_set(prefix_path + ['mode', 'interface']) + self.cli_set(prefix_path + ['interface', interface]) + self.cli_commit() + + config = getConfigSection(f'proxy {interface}') + self.assertIn('rule 2001:db8::/64 {', config) + self.assertIn(f'iface {interface}', config) + + def test_prefix_mode_auto_rejects_interface(self): + interface = Section.interfaces('ethernet')[0] + prefix_path = base_path + ['interface', interface, 'prefix', '2001:db8::/64'] + self.cli_set(base_path + ['interface', interface]) + self.cli_commit() + + self.cli_set(prefix_path + ['mode', 'auto']) + self.cli_set(prefix_path + ['interface', interface]) + + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + def test_disabled_prefix_skips_validation(self): + interface = Section.interfaces('ethernet')[0] + prefix_path = base_path + ['interface', interface, 'prefix', '2001:db8::/64'] + self.cli_set(base_path + ['interface', interface]) + self.cli_set(prefix_path + ['mode', 'interface']) + self.cli_set(prefix_path + ['disable']) + + self.cli_commit() + + config = getConfigSection(f'proxy {interface}') + self.assertNotIn('rule 2001:db8::/64 {', config) + + def test_disabled_interface_skips_validation(self): + interface = Section.interfaces('ethernet')[0] + prefix_path = base_path + ['interface', interface, 'prefix', '2001:db8::/64'] + self.cli_set(base_path + ['interface', interface, 'disable']) + self.cli_set(prefix_path + ['mode', 'interface']) + + self.cli_commit() + + config = cmd(f'cat {NDPPD_CONF}') + self.assertNotIn(f'proxy {interface} {{', config) + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_ntp.py b/smoketest/scripts/cli/test_service_ntp.py index 469d44eaa..deeae4708 100755 --- a/smoketest/scripts/cli/test_service_ntp.py +++ b/smoketest/scripts/cli/test_service_ntp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,6 +19,7 @@ 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 @@ -38,11 +39,12 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.assertTrue(process_named_running(PROCESS_NAME)) - self.cli_delete(base_path) self.cli_commit() - + # Check for no longer running process self.assertFalse(process_named_running(PROCESS_NAME)) + # always forward to base class + super().tearDown() def test_base_options(self): # Test basic NTP support with multiple servers and their options @@ -63,7 +65,7 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): # Check generated configuration # this file must be read with higher permissions - config = cmd(f'sudo cat {NTP_CONF}') + config = read_file(NTP_CONF, sudo=True) self.assertIn('driftfile /run/chrony/drift', config) self.assertIn('dumpdir /run/chrony', config) self.assertIn('ntsdumpdir /run/chrony', config) @@ -78,6 +80,18 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): for pool in pools: self.assertIn(f'pool {pool} iburst', config) + def test_local_stratum_without_upstream_server(self): + stratum = '10' + network = '192.0.2.0/24' + + self.cli_set(base_path + ['local-stratum', stratum]) + self.cli_set(base_path + ['allow-client', 'address', network]) + self.cli_commit() + + config = read_file(NTP_CONF, sudo=True) + self.assertIn(f'local stratum {stratum}', config) + self.assertIn(f'allow {network}', config) + def test_clients(self): # Test the allowed-networks statement listen_address = ['127.0.0.1', '::1'] @@ -88,19 +102,10 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): for network in networks: self.cli_set(base_path + ['allow-client', 'address', network]) - # Verify "NTP server not configured" verify() statement - with self.assertRaises(ConfigSessionError): - self.cli_commit() - - servers = ['192.0.2.1', '192.0.2.2'] - for server in servers: - self.cli_set(base_path + ['server', server]) - self.cli_commit() # Check generated client address configuration - # this file must be read with higher permissions - config = cmd(f'sudo cat {NTP_CONF}') + config = read_file(NTP_CONF, sudo=True) for network in networks: self.assertIn(f'allow {network}', config) @@ -120,8 +125,7 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Check generated client address configuration - # this file must be read with higher permissions - config = cmd(f'sudo cat {NTP_CONF}') + config = read_file(NTP_CONF, sudo=True) for interface in interfaces: self.assertIn(f'binddevice {interface}', config) @@ -151,14 +155,13 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Check generated client address configuration - # this file must be read with higher permissions - config = cmd(f'sudo cat {NTP_CONF}') + config = read_file(NTP_CONF, sudo=True) self.assertIn('leapsectz right/UTC', config) # CLI default for mode in ['ignore', 'system', 'smear']: self.cli_set(base_path + ['leap-second', mode]) self.cli_commit() - config = cmd(f'sudo cat {NTP_CONF}') + config = read_file(NTP_CONF, sudo=True) if mode != 'smear': self.assertIn(f'leapsecmode {mode}', config) else: @@ -181,8 +184,7 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Check generated configuration - # this file must be read with higher permissions - config = cmd(f'sudo cat {NTP_CONF}') + config = read_file(NTP_CONF, sudo=True) self.assertIn('driftfile /run/chrony/drift', config) self.assertIn('dumpdir /run/chrony', config) self.assertIn('ntsdumpdir /run/chrony', config) @@ -209,8 +211,7 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Check generated configuration - # this file must be read with higher permissions - config = cmd(f'sudo cat {NTP_CONF}') + config = read_file(NTP_CONF, sudo=True) self.assertIn('driftfile /run/chrony/drift', config) self.assertIn('dumpdir /run/chrony', config) self.assertIn('ntsdumpdir /run/chrony', config) @@ -245,8 +246,7 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Check generated configuration - # this file must be read with higher permissions - config = cmd(f'sudo cat {NTP_CONF}') + config = read_file(NTP_CONF, sudo=True) self.assertIn('driftfile /run/chrony/drift', config) self.assertIn('dumpdir /run/chrony', config) self.assertIn('ntsdumpdir /run/chrony', config) @@ -261,4 +261,4 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): self.assertIn(f'ptpport {default_ptp_port}', config) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_pppoe-server.py b/smoketest/scripts/cli/test_service_pppoe-server.py index 8cd87e0f2..1a06e050f 100755 --- a/smoketest/scripts/cli/test_service_pppoe-server.py +++ b/smoketest/scripts/cli/test_service_pppoe-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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_accel_ppp_test import BasicAccelPPPTest +from base_vyostest_shim import VyOSUnitTestSHIM from configparser import ConfigParser from vyos.utils.file import read_file @@ -39,6 +40,7 @@ class TestServicePPPoEServer(BasicAccelPPPTest.TestCase): def tearDown(self): self.cli_delete(local_if) + # always forward to base class super().tearDown() def verify(self, conf): @@ -213,4 +215,4 @@ class TestServicePPPoEServer(BasicAccelPPPTest.TestCase): if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_router-advert.py b/smoketest/scripts/cli/test_service_router-advert.py index 6dbb6add4..52f0db4ab 100755 --- a/smoketest/scripts/cli/test_service_router-advert.py +++ b/smoketest/scripts/cli/test_service_router-advert.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2022 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -55,12 +55,12 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase): def tearDown(self): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) - self.cli_delete(base_path) self.cli_commit() - # Check for no longer running process self.assertFalse(process_named_running(PROCESS_NAME)) + # always forward to base class + super().tearDown() def test_common(self): self.cli_set(base_path + ['prefix', prefix, 'no-on-link-flag']) @@ -224,6 +224,17 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase): self.assertIn(tmp, config) self.assertIn('AdvValidLifetime 65528;', config) # default + def test_captive_portal(self): + captive_portal = 'https://example.com/api/capport.json' + + self.cli_set(base_path + ['captive-portal', captive_portal]) + # commit changes + self.cli_commit() + + # Verify generated configuration + tmp = get_config_value('AdvCaptivePortalAPI') + self.assertEqual(tmp, f'"{captive_portal}"') + def test_advsendadvert_advintervalopt(self): ra_src = ['fe80::1', 'fe80::2'] @@ -252,6 +263,132 @@ 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) + + def test_base_interface(self): + self.cli_set(base_path + ['prefix', '::/64']) + self.cli_set(base_path + ['prefix', '::/64', 'base-interface', 'eth0']) + self.cli_commit() + + config = read_file(RADVD_CONF) + self.assertIn('Base6Interface eth0;', config) + + self.cli_set(base_path + ['prefix', '2001:db8:1234::/64']) + self.cli_set( + base_path + ['prefix', '2001:db8:1234::/64', 'base-interface', 'eth0'] + ) + with self.assertRaises(ConfigSessionError): + self.cli_commit() if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_salt-minion.py b/smoketest/scripts/cli/test_service_salt-minion.py index 48a588b72..494e02aa5 100755 --- a/smoketest/scripts/cli/test_service_salt-minion.py +++ b/smoketest/scripts/cli/test_service_salt-minion.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022-2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -23,7 +23,6 @@ from vyos.utils.process import process_named_running from vyos.utils.file import read_file from vyos.utils.process import cmd -PROCESS_NAME = 'salt-minion' SALT_CONF = '/etc/salt/minion' base_path = ['service', 'salt-minion'] @@ -47,7 +46,7 @@ class TestServiceSALT(VyOSUnitTestSHIM.TestCase): def tearDown(self): # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.assertTrue(process_named_running('python3.11', '/usr/bin/salt-minion')) # delete testing SALT config self.cli_delete(base_path) @@ -57,7 +56,9 @@ class TestServiceSALT(VyOSUnitTestSHIM.TestCase): # from the CI) salt-minion process is not killed by systemd. Apparently # no issue on VMWare. if cmd('systemd-detect-virt') != 'kvm': - self.assertFalse(process_named_running(PROCESS_NAME)) + self.assertFalse(process_named_running('python3.11', '/usr/bin/salt-minion')) + # always forward to base class + super().tearDown() def test_default(self): servers = ['192.0.2.1', '192.0.2.2'] @@ -102,4 +103,4 @@ class TestServiceSALT(VyOSUnitTestSHIM.TestCase): self.assertIn(f'source_interface_name: {interface}', conf) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_snmp.py b/smoketest/scripts/cli/test_service_snmp.py index 7d5eaa440..1f3a7a372 100755 --- a/smoketest/scripts/cli/test_service_snmp.py +++ b/smoketest/scripts/cli/test_service_snmp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2021 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -20,16 +20,22 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError -from vyos.template import is_ipv4 +from vyos.defaults import systemd_services from vyos.template import address_from_cidr +from vyos.template import bracketize_ipv6 +from vyos.template import is_ipv4 +from vyos.template import is_ipv6 +from vyos.utils.process import cmd from vyos.utils.process import call from vyos.utils.process import DEVNULL from vyos.utils.file import read_file from vyos.utils.process import process_named_running from vyos.version import get_version_data +from vyos.xml_ref import default_value PROCESS_NAME = 'snmpd' SNMPD_CONF = '/etc/snmp/snmpd.conf' +SYSTEMD_SERVICE = systemd_services['snmpd'] base_path = ['service', 'snmp'] @@ -46,6 +52,20 @@ def get_config_value(key): tmp = re.findall(r'\n?{}\s+(.*)'.format(key), tmp) return tmp[0] + +def get_engine_boots() -> int: + """Query engineBoots directly from the running snmpd via SNMP""" + + engine_boots_oid = '1.3.6.1.6.3.10.2.1.2.0' + out = cmd( + f'snmpget -v3 -u {snmpv3_user} -l authPriv ' + f'-a SHA -A {snmpv3_auth_pw} ' + f'-x AES -X {snmpv3_priv_pw} ' + f'127.0.0.1 {engine_boots_oid}' + ) + # Output: SNMP-FRAMEWORK-MIB::snmpEngineBoots.0 = INTEGER: 3 + return int(out.split()[-1]) if 'snmpEngineBoots' in out else 0 + class TestSNMPService(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): @@ -58,13 +78,13 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase): def tearDown(self): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) - # delete testing SNMP config self.cli_delete(base_path) self.cli_commit() - # Check for running process self.assertFalse(process_named_running(PROCESS_NAME)) + # always forward to base class + super().tearDown() def test_snmp_basic(self): dummy_if = 'dum7312' @@ -98,7 +118,7 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase): # verify listen address, it will be returned as # ['unix:/run/snmpd.socket,udp:127.0.0.1:161,udp6:[::1]:161'] - # thus we need to transfor this into a proper list + # thus we need to transform this into a proper list config = get_config_value('agentaddress') expected = 'unix:/run/snmpd.socket' self.assertIn(expected, config) @@ -199,7 +219,7 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['v3', 'user', 'vyos', 'group', snmpv3_group]) self.cli_set(base_path + ['v3', 'group', snmpv3_group, 'mode', 'ro']) - # check validate() - a view must be created before this can be comitted + # check validate() - a view must be created before this can be committed with self.assertRaises(ConfigSessionError): self.cli_commit() @@ -246,6 +266,36 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase): for excluded in snmpv3_view_oid_exclude: self.assertIn(f'view {snmpv3_view} excluded .{excluded}', tmp) + def test_snmpv3_trap(self): + trap_targets = ['192.0.2.55', '2001:db8::1'] + + self.cli_set(base_path + ['v3', 'engineid', snmpv3_engine_id]) + self.cli_set(base_path + ['v3', 'group', snmpv3_group, 'view', snmpv3_view]) + self.cli_set(base_path + ['v3', 'view', snmpv3_view, 'oid', snmpv3_view_oid]) + + for trap_target in trap_targets: + trap_base = base_path + ['v3', 'trap-target', trap_target] + + self.cli_set(trap_base + ['auth', 'plaintext-password', snmpv3_auth_pw]) + self.cli_set(trap_base + ['auth', 'type', 'sha']) + self.cli_set(trap_base + ['privacy', 'plaintext-password', snmpv3_priv_pw]) + self.cli_set(trap_base + ['privacy', 'type', 'aes']) + self.cli_set(trap_base + ['type', 'trap']) + self.cli_set(trap_base + ['user', snmpv3_user]) + + self.cli_commit() + + tmp = read_file(SNMPD_CONF) + for trap_target in trap_targets: + cli_default_trap_port = default_value(base_path + ['v3', 'trap-target', trap_target, 'port']) + cli_default_trap_protocol = default_value(base_path + ['v3', 'trap-target', trap_target, 'protocol']) + if is_ipv6(trap_target): + cli_default_trap_protocol = f'{cli_default_trap_protocol}6' + + self.assertIn(f'trapsess -v 3 -e "{snmpv3_engine_id}" -u {snmpv3_user} -a SHA -A {snmpv3_auth_pw} ' \ + f'-x AES -X {snmpv3_priv_pw} -l authPriv ' \ + f'{cli_default_trap_protocol}:{bracketize_ipv6(trap_target)}:{cli_default_trap_port}', tmp) + def test_snmp_script_extensions(self): extensions = { 'default': 'snmp_smoketest_extension_script.sh', @@ -259,6 +309,105 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase): self.assertEqual(get_config_value('extend default'), f'/config/user-data/{extensions["default"]}') self.assertEqual(get_config_value('extend external'), extensions["external"]) + def test_snmp_engine_boots_increment(self): + # T8538: engineBoots must increment by 1 on every snmpd restart. + + snmpd_file = '/var/lib/snmp/snmpd.conf' + persist_file = '/config/snmp/engineboots.count' + + def _verify_engine_boots(value_before, value_after): + lib_snmpd_content = read_file(snmpd_file, sudo=True) + persist_count_content = read_file(persist_file) + + with self.subTest(value_before=value_before, value_after=value_after): + self.assertGreater( + value_after, + value_before, + 'engineBoots must increase after snmpd restart', + ) + self.assertIn( + f'engineBoots {value_after}\n', + lib_snmpd_content, + f'{snmpd_file} does not contain `engineBoots {value_after}`', + ) + self.assertEqual( + str(value_after), + persist_count_content, + f'{persist_file} does not match the expected value `{value_after}`', + ) + + self.cli_set(base_path + ['v3', 'engineid', snmpv3_engine_id]) + self.cli_set(base_path + ['v3', 'group', 'default', 'mode', 'ro']) + self.cli_set(base_path + ['v3', 'view', 'default', 'oid', '1']) + self.cli_set(base_path + ['v3', 'group', 'default', 'view', 'default']) + + base_user_path = base_path + ['v3', 'user', snmpv3_user] + self.cli_set(base_user_path + ['auth', 'plaintext-password', snmpv3_auth_pw]) + self.cli_set(base_user_path + ['auth', 'type', 'sha']) + self.cli_set(base_user_path + ['privacy', 'plaintext-password', snmpv3_priv_pw]) + self.cli_set(base_user_path + ['privacy', 'type', 'aes']) + self.cli_set(base_user_path + ['group', 'default']) + self.cli_commit() + + value_before = get_engine_boots() + + # Simulates multiple commits (which stop/start snmpd) and checks + # the live OID value increases monotonically. + self.cli_set(base_path + ['v3', 'view', 'default', 'oid', '2']) + self.cli_commit() + + value_after = get_engine_boots() + _verify_engine_boots(value_before, value_after) + + value_before = get_engine_boots() + + # Restart of the service also should trigger changing of engineBoots + call(f'sudo systemctl restart {SYSTEMD_SERVICE}') + + value_after = get_engine_boots() + _verify_engine_boots(value_before, value_after) + + def test_snmp_engine_boots_reset(self): + # T8538: engineBoots should be set to zero on every changing of engineID + + self.cli_set(base_path + ['v3', 'engineid', snmpv3_engine_id]) + self.cli_set(base_path + ['v3', 'group', 'default', 'mode', 'ro']) + self.cli_set(base_path + ['v3', 'view', 'default', 'oid', '1']) + self.cli_set(base_path + ['v3', 'group', 'default', 'view', 'default']) + + base_user_path = base_path + ['v3', 'user', snmpv3_user] + self.cli_set(base_user_path + ['auth', 'plaintext-password', snmpv3_auth_pw]) + self.cli_set(base_user_path + ['auth', 'type', 'sha']) + self.cli_set(base_user_path + ['privacy', 'plaintext-password', snmpv3_priv_pw]) + self.cli_set(base_user_path + ['privacy', 'type', 'aes']) + self.cli_set(base_user_path + ['group', 'default']) + self.cli_commit() + + # Restart of the service to trigger changing of engineBoots + call(f'sudo systemctl restart {SYSTEMD_SERVICE}') + + value_before = get_engine_boots() + self.assertGreater( + value_before, + 1, + f'engineBoots should be greater 1 after restart of {SYSTEMD_SERVICE}', + ) + + new_snmpv3_engine_id = '000000000000000000000004' + self.cli_set(base_path + ['v3', 'engineid', new_snmpv3_engine_id]) + # Re-add passwords because they were hashed by old engine id + self.cli_set(base_user_path + ['auth', 'plaintext-password', snmpv3_auth_pw]) + self.cli_set(base_user_path + ['privacy', 'plaintext-password', snmpv3_priv_pw]) + self.cli_commit() + + value_after = get_engine_boots() + self.assertEqual( + value_after, + 1, + 'engineBoots should be set to zero on every changing of engineID', + ) + + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_ssh.py b/smoketest/scripts/cli/test_service_ssh.py index fa08a5b32..98dd90d8a 100755 --- a/smoketest/scripts/cli/test_service_ssh.py +++ b/smoketest/scripts/cli/test_service_ssh.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,15 +19,16 @@ import paramiko import re import unittest -from pwd import getpwall - from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError +from vyos.defaults import config_files +from vyos.utils.auth import get_local_passwd_entries from vyos.utils.process import cmd from vyos.utils.process import is_systemd_service_running from vyos.utils.process import process_named_running from vyos.utils.file import read_file +from vyos.utils.file import write_file from vyos.xml_ref import default_value PROCESS_NAME = 'sshd' @@ -38,26 +39,101 @@ 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' - +trusted_user_ca = config_files['sshd_user_ca'] +test_command = 'uname -a' def get_config_value(key): tmp = read_file(SSHD_CONF) tmp = re.findall(f'\n?{key}\s+(.*)', tmp) return tmp +trusted_user_ca_path = base_path + ['trusted-user-ca'] +# CA and signed user key generated using: +# ssh-keygen -f vyos-ssh-ca.key +# ssh-keygen -f vyos_testca -C "vyos_tesca@vyos.net" +# ssh-keygen -s vyos-ssh-ca.key -I vyos_testca@vyos.net -n vyos,vyos_testca -V +520w vyos_testca.pub +ca_cert_data = """ +AAAAB3NzaC1yc2EAAAADAQABAAABgQCTBa7+TTefsMLTHuuLPUmmm7SGAuoK03oZEIi2/O +sww1uhCdKrm7bFvSUFpWvq3gX8TSS+yO5kNKz3BTMBu7oq01/Ewjyw0jR+fUog76x7mCzd +2iI4QmPj4lNHSUFquaELt2aBwY4f7LtjxRCCgtWgirq/Qk+P27uJKErvndyYc95v9no15z +lQFSdUid6tF8IjYljK8pXP0JshFp3XnFV2Rg80j7O66mRtVFC4tt2vluyIFeIID+5fL03v +LXbT/2zNdoH6QiI9NGWkxhS7zFYziVd/rzG5xlEB1ezs2Sz4zjMPgV3GiMINb6tjEWNJhM +KtDWIt+3UDpx+2T9PrhDBDFMlneiHCD6MxRv2sLbicevSj0PV7/fRnwoHs6hDKCU5eS2Mc +CTxXr4jaboLZ6q3sbGHCHZo/PuA8Sl9iZCM4GCxx5bgvRRmGpgZv4PfFzA2b/wTHkKnf6E +kuthoAJufmNxPaZQRQKF34SdmTKgSJTCY1gqwCH2iNg0PVKU+vN8c= +""" -ca_root_cert_data = """ -MIIBcTCCARagAwIBAgIUDcAf1oIQV+6WRaW7NPcSnECQ/lUwCgYIKoZIzj0EAwIw -HjEcMBoGA1UEAwwTVnlPUyBzZXJ2ZXIgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjBa -Fw0zMjAyMTUxOTQxMjBaMB4xHDAaBgNVBAMME1Z5T1Mgc2VydmVyIHJvb3QgQ0Ew -WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ0y24GzKQf4aM2Ir12tI9yITOIzAUj -ZXyJeCmYI6uAnyAMqc4Q4NKyfq3nBi4XP87cs1jlC1P2BZ8MsjL5MdGWozIwMDAP -BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRwC/YaieMEnjhYa7K3Flw/o0SFuzAK -BggqhkjOPQQDAgNJADBGAiEAh3qEj8vScsjAdBy5shXzXDVVOKWCPTdGrPKnu8UW -a2cCIQDlDgkzWmn5ujc5ATKz1fj+Se/aeqwh4QyoWCVTFLIxhQ== +cert_user_key = """-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn +NhAAAAAwEAAQAAAYEArnIlFpMwSQax7+qH3+/gbv65mem6Ur+gepNYC8TYaE91xJxMoE5M +Pyh1s8Kr/WYNF6aN43qdDnjvGy38oFng4lEfxG475AqpTIGmP4GvEOlnNLhjCcOHrOFuzg +uRtDDvn0/TPhdqLTlbvgZ326WO7xQkCX11qmdGUUtC9Byd7p+EmnTe0oP8N6MeyYY78qa4 +HnzMd6EPb3vyWdASpPZjQE0OJCeAx6Mne2kOnKxUcW1UlczOa1PPIQMU+Rp1PWDtkdiYAd +nbTbIdxDN8Bn3mC3JXD642EcwXSJ1+kov/8u8bBuYNt3t3nf/krSebx4Ge7ObYnURj31j0 +8L8Vv3fgv+T7pY8iyMh8dYfrZPAWQGN1pe8ZkDaM1QGKJncF+8N0UB4EVFBHNLt7W8+oHt +LPMqYw13djZHg5Q1NxSxc1srOmEBZrWCBZgDGGiqtKo+lF+oVvqvBh/hncOBlDX5RFM8qw +Qt4mem9TEZZrIvC9q1dcVpQUrt8BvBOSnGnBb7yTAAAFkEdBIUlHQSFJAAAAB3NzaC1yc2 +EAAAGBAK5yJRaTMEkGse/qh9/v4G7+uZnpulK/oHqTWAvE2GhPdcScTKBOTD8odbPCq/1m +DRemjeN6nQ547xst/KBZ4OJRH8RuO+QKqUyBpj+BrxDpZzS4YwnDh6zhbs4LkbQw759P0z +4Xai05W74Gd9ulju8UJAl9dapnRlFLQvQcne6fhJp03tKD/DejHsmGO/KmuB58zHehD297 +8lnQEqT2Y0BNDiQngMejJ3tpDpysVHFtVJXMzmtTzyEDFPkadT1g7ZHYmAHZ202yHcQzfA +Z95gtyVw+uNhHMF0idfpKL//LvGwbmDbd7d53/5K0nm8eBnuzm2J1EY99Y9PC/Fb934L/k ++6WPIsjIfHWH62TwFkBjdaXvGZA2jNUBiiZ3BfvDdFAeBFRQRzS7e1vPqB7SzzKmMNd3Y2 +R4OUNTcUsXNbKzphAWa1ggWYAxhoqrSqPpRfqFb6rwYf4Z3DgZQ1+URTPKsELeJnpvUxGW +ayLwvatXXFaUFK7fAbwTkpxpwW+8kwAAAAMBAAEAAAGAEeZQe+0vyoPPWkjRwbQBbszgX9 +9QaRE/TD82N5mZLbWJkK+2WnSY9O9tNGbIncBiSNz5ji/p/FmDCgzr8SAyfRvJ4K6sTTfy +1eYvwtscYDsy2ywDAuDMrnvrPLqJ1tghSP2N4BR9ppT4yZosTkjB+TIzMxjBLB0GEBgNj1 +19rxswe2YmlFSgBVgi3pbRgT0uLfgBmvzXHUoLPL/8ScT7u4Csmh/GN7Xmuo5gcMnArcAu +1Q17g3PJZcpv1Ser2VfKnVAwrURCLW8dlji5xat/3E/PLsrLvszVS6U0hFf3MaOixprxsz +wc0n2Y4lAgkgkCZQ0Ty9TSXI/8TQWL8cPFej1TK15NWXlfElZxI+lhwcsnWmNy3mXD746/ +YZLH+OCs9isvewZWryQEkdVCU42MM/7L4Hoeqh2diGDV9wtKDW5FjHq/VRNOMVt59eCFlv +eujh89/KY6wPxHoDoY3+olhggiKDGw1wUUpEXKNQhhTjx1g0xn7AFYz+Bp2svM9EdhAAAA +wQDBq+zeOhsS/VrrVRkmOYYXnBSe0WcckjcYOly/8FLTPkq19aVY5eOmo6teegqvkWscGP +Wisl7DW+kFNolIvwc6shf/8+PXC1KlADd9S1uoXvSmVoe3wSsIKRCsUuLZiiJkv4nqQ/BK +T6ijvNG2Wu3YGsP8Tj+OcTebqk1vDItaickhKtFxCx6PBcV+RrDeK1TT6uAHd1AsGikTva +V/BDMmtoDz7qFQbj9Vj2np88MakxYfm7u4DzKu082GHDBC44sAAADBAN8ATvmmfxqk5GFg ++2rbIW+qMJ2GwWXiTFLjH7u4HEhsmHbHYsQ0v+cGu2dKfBUVWoq/N2ltDQ0QYTgkmsxKvm +I8AjVhLHhFB1DtPBMHibsF/rtBRgsItR+PveUtRYOmeY1PzJ3ygVNJpPJ87st0T4JVNQiE ++bFEhnJ/RcTHxzAAt8+gTn0PTen3+hn9Jk2YFHWFb51YDw2h00LL9XT9Enz4xkc6gTPL3M +0IKULJWnyYGOLueSsQxJiaAUcsZg8W2QAAAMEAyEJ45HtbUqZ5xd2K5ZfY8cd1dC9uAx6a +cSdENUvMW4yE3QEJ4xdonDUn9OQYR7GpseQWuXBrTO2PSsse7P6eHUsRhaUkFOvLzHSVzO +bI9HDJAq6+KCPhm2eixfBiMs2meEle8MvNiiONwaY3JnPnGdsTpEjcm6oulyC52xRvHhvc +nCuoRTqX7xcIka4jCXInYBS7GhlF5iAmIAAVkvfWjjNwZ3S0mnGUUOYgknidBhK+x0zCWt +IXOeoIfjb/C4NLAAAAE3Z5b3NfdGVzY2FAdnlvcy5uZXQBAgMEBQYH +-----END OPENSSH PRIVATE KEY----- """ +cert_user_signed = """ +ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb2 +0AAAAglE+kjRPqsck/y2ywO+owv1FTeU6QFNPywFqD8aoEcA8AAAADAQABAAABgQCuciUWk +zBJBrHv6off7+Bu/rmZ6bpSv6B6k1gLxNhoT3XEnEygTkw/KHWzwqv9Zg0Xpo3jep0OeO8b +LfygWeDiUR/EbjvkCqlMgaY/ga8Q6Wc0uGMJw4es4W7OC5G0MO+fT9M+F2otOVu+BnfbpY7 +vFCQJfXWqZ0ZRS0L0HJ3un4SadN7Sg/w3ox7JhjvyprgefMx3oQ9ve/JZ0BKk9mNATQ4kJ4 +DHoyd7aQ6crFRxbVSVzM5rU88hAxT5GnU9YO2R2JgB2dtNsh3EM3wGfeYLclcPrjYRzBdIn +X6Si//y7xsG5g23e3ed/+StJ5vHgZ7s5tidRGPfWPTwvxW/d+C/5PuljyLIyHx1h+tk8BZA +Y3Wl7xmQNozVAYomdwX7w3RQHgRUUEc0u3tbz6ge0s8ypjDXd2NkeDlDU3FLFzWys6YQFmt +YIFmAMYaKq0qj6UX6hW+q8GH+Gdw4GUNflEUzyrBC3iZ6b1MRlmsi8L2rV1xWlBSu3wG8E5 +KcacFvvJMAAAAAAAAAAAAAAAEAAAAUdnlvc190ZXN0Y2FAdnlvcy5uZXQAAAAXAAAABHZ5b +3MAAAALdnlvc190ZXN0Y2EAAAAAaDg66AAAAAB69w9WAAAAAAAAAIIAAAAVcGVybWl0LVgx +MS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGV +ybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LX +VzZXItcmMAAAAAAAAAAAAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAJMFrv5NN5+wwtMe6 +4s9SaabtIYC6grTehkQiLb86zDDW6EJ0qubtsW9JQWla+reBfxNJL7I7mQ0rPcFMwG7uirT +X8TCPLDSNH59SiDvrHuYLN3aIjhCY+PiU0dJQWq5oQu3ZoHBjh/su2PFEIKC1aCKur9CT4/ +bu4koSu+d3Jhz3m/2ejXnOVAVJ1SJ3q0XwiNiWMrylc/QmyEWndecVXZGDzSPs7rqZG1UUL +i23a+W7IgV4ggP7l8vTe8tdtP/bM12gfpCIj00ZaTGFLvMVjOJV3+vMbnGUQHV7OzZLPjOM +w+BXcaIwg1vq2MRY0mEwq0NYi37dQOnH7ZP0+uEMEMUyWd6IcIPozFG/awtuJx69KPQ9Xv9 +9GfCgezqEMoJTl5LYxwJPFeviNpugtnqrexsYcIdmj8+4DxKX2JkIzgYLHHluC9FGYamBm/ +g98XMDZv/BMeQqd/oSS62GgAm5+Y3E9plBFAoXfhJ2ZMqBIlMJjWCrAIfaI2DQ9UpT683xw +AAAZQAAAAMcnNhLXNoYTItNTEyAAABgINZAr9M9ZYWDhhf5uWNkUBKq12OlJ3ImvHg5161P +BAAL6crGS3WzyAs9LerxFcdMJ0gzMgUixR59MgGMAzfN+DjoSmgcLVT0eVoI5GMBkdiq8T5 +h3qjeXTc5BfLJiACbu7tOPhuIsIDreDnCVYmGr2z+rAPaqMETJa4L0submx4DqnahSY0ZSH +WjTrjWCSPIdySh9HUXbpq3tYdNlqmpSY5YzvDmMC46kGMF10G5ycc58asWfUMwLMGsTEt2t +R5DKRDw/iJch3r+L0xLMCSmEXnu6/Gl7Yq1XJdWm9cA1SvDyxEuB4yKIDkunXrPiuPn3zyv +z1a/bY0hvuF+fyL+tRCbmrfOLreHuYh9aFg6e22MoKhrez5wP8Eoy1T+rlQrmlgCRDShBgj +wMMhc+2fdrzTR07Ctnmv339p/SY5wBruzNM9R1mzyEuuJDE6OkKBTI8kuQu6ypGv+bLqSSt +wujcNqOI4Vz61HiOsRSTUa7tA5q4hBwFqq7FB8+N0Ylfa5A== vyos_tesca@vyos.net +""" class TestServiceSSH(VyOSUnitTestSHIM.TestCase): @classmethod @@ -86,6 +162,8 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase): # We can not use process_named_running here - we rather need to check # that the systemd service is no longer running self.assertFalse(is_systemd_service_running(PROCESS_NAME)) + # always forward to base class + super().tearDown() def test_ssh_default(self): # Check if SSH service runs with default settings - used for checking @@ -95,7 +173,7 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase): # commit changes self.cli_commit() - # Check configured port agains CLI default value + # Check configured port against CLI default value port = get_config_value('Port') cli_default = default_value(base_path + ['port']) self.assertEqual(port, cli_default) @@ -207,23 +285,12 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase): # run natively. # # We also try to login as an invalid user - this is not allowed to work. - test_user = 'ssh_test' test_pass = 'v2i57DZs8idUwMN3VC92' - 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() @@ -236,15 +303,14 @@ 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() # After deletion the test user is not allowed to remain in /etc/passwd - usernames = [x[0] for x in getpwall()] + usernames = [x.pw_name for x in get_local_passwd_entries()] self.assertNotIn(test_user, usernames) def test_ssh_dynamic_protection(self): @@ -311,7 +377,7 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase): rekey_data = '1024' for cipher in ciphers: - self.cli_set(base_path + ['ciphers', cipher]) + self.cli_set(base_path + ['cipher', cipher]) for host_key in host_key_algs: self.cli_set(base_path + ['hostkey-algorithm', host_key]) for kex in kexes: @@ -359,40 +425,91 @@ 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): + def test_ssh_trusted_user_ca(self): ca_cert_name = 'test_ca' + public_key_type = 'ssh-rsa' + public_key_data = ca_cert_data.replace('\n', '') + test_user = 'vyos_testca' + principal = 'vyos' + user_auth_base = ['system', 'login', 'user', test_user] + + # create user account + self.cli_set(user_auth_base) + self.cli_set(pki_path + ['openssh', ca_cert_name, 'public', + 'key', public_key_data]) + self.cli_set(pki_path + ['openssh', ca_cert_name, 'public', + 'type', public_key_type]) + self.cli_set(trusted_user_ca_path, value=ca_cert_name) + self.cli_commit() - # 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] - ) + trusted_user_ca_config = get_config_value('TrustedUserCAKeys') + self.assertIn(trusted_user_ca, trusted_user_ca_config) + + authorize_principals_file_config = get_config_value('AuthorizedPrincipalsFile') + self.assertIn('none', authorize_principals_file_config) + + ca_key_contents = read_file(trusted_user_ca).lstrip().rstrip() + self.assertIn(f'{public_key_type} {public_key_data}', ca_key_contents) + + # Verify functionality by logging into the system using signed user key + key_filename = f'/tmp/{test_user}' + write_file(key_filename, cert_user_key, mode=0o600) + write_file(f'{key_filename}-cert.pub', cert_user_signed.replace('\n', '')) + + # Login with proper credentials + output, error = self.ssh_send_cmd(test_command, test_user, password=None, + key_filename=key_filename) + # Verify login + self.assertFalse(error) + self.assertEqual(output, cmd(test_command)) + + # Enable user principal name - logins only allowed if certificate contains + # said principal name + self.cli_set(user_auth_base + ['authentication', 'principal', principal]) self.cli_commit() - trusted_user_ca_key_config = get_config_value('TrustedUserCAKeys') - self.assertIn(trusted_user_ca_key, trusted_user_ca_key_config) + # Verify generated SSH principals + authorized_principals_file = f'/home/{test_user}/.ssh/authorized_principals' + authorized_principals = read_file(authorized_principals_file, sudo=True) + self.assertIn(principal, authorized_principals) - with open(trusted_user_ca_key, 'r') as file: - ca_key_contents = file.read() - self.assertIn(ca_root_cert_data, ca_key_contents) + # Login with proper credentials + output, error = self.ssh_send_cmd(test_command, test_user, password=None, + key_filename=key_filename) + # Verify login + self.assertFalse(error) + self.assertEqual(output, cmd(test_command)) - self.cli_delete(base_path + ['trusted-user-ca-key']) + self.cli_delete(trusted_user_ca_path) + self.cli_delete(user_auth_base) 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) + trusted_user_ca_config = get_config_value('TrustedUserCAKeys') + self.assertNotIn(trusted_user_ca, trusted_user_ca_config) + self.assertFalse(os.path.exists(trusted_user_ca)) + + authorize_principals_file_config = get_config_value('AuthorizedPrincipalsFile') + self.assertNotIn('none', authorize_principals_file_config) + self.assertFalse(os.path.exists(f'/home/{test_user}/.ssh/authorized_principals')) + + def test_ssh_fido(self): + # Order does matter for this test because of how the template + # collects and maps the options. + opt_map = { + 'pin-required': 'verify-required', + 'touch-required': 'touch-required', + } + expected = 'PubkeyAuthOptions ' + for k, v in opt_map.items(): + self.cli_set(base_path + ['fido', k]) + expected = f'{expected}{v} ' + expected = expected[:-1] + self.cli_commit() + tmp_sshd_conf = read_file(SSHD_CONF) + self.assertIn(expected, tmp_sshd_conf) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_stunnel.py b/smoketest/scripts/cli/test_service_stunnel.py index 3aeffd09e..4bfa22659 100755 --- a/smoketest/scripts/cli/test_service_stunnel.py +++ b/smoketest/scripts/cli/test_service_stunnel.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -182,6 +182,8 @@ class TestServiceStunnel(VyOSUnitTestSHIM.TestCase): # Check for stopped process self.assertFalse(process_named_running(PROCESS_NAME)) + # always forward to base class + super().tearDown() def set_pki(self): self.cli_set(['pki', 'ca', 'ca-1', 'certificate', ca_certificate.replace('\n','')]) @@ -621,4 +623,4 @@ class TestServiceStunnel(VyOSUnitTestSHIM.TestCase): if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_tftp-server.py b/smoketest/scripts/cli/test_service_tftp-server.py index d60794980..d001b3671 100755 --- a/smoketest/scripts/cli/test_service_tftp-server.py +++ b/smoketest/scripts/cli/test_service_tftp-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -52,12 +52,12 @@ class TestServiceTFTPD(VyOSUnitTestSHIM.TestCase): def tearDown(self): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) - self.cli_delete(base_path) self.cli_commit() - # Check for no longer running process self.assertFalse(process_named_running(PROCESS_NAME)) + # always forward to base class + super().tearDown() def test_01_tftpd_single(self): directory = '/tmp' @@ -106,7 +106,7 @@ class TestServiceTFTPD(VyOSUnitTestSHIM.TestCase): self.assertIn(directory, config) # Check for running processes - one process is spawned per listen - # IP address, wheter it's IPv4 or IPv6 + # IP address, whether it's IPv4 or IPv6 count = 0 for p in process_iter(): if PROCESS_NAME in p.name(): @@ -148,4 +148,4 @@ class TestServiceTFTPD(VyOSUnitTestSHIM.TestCase): self.cli_delete(['vrf', 'name', vrf]) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_service_webproxy.py b/smoketest/scripts/cli/test_service_webproxy.py index ab4707a61..7f3731800 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-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -45,6 +45,8 @@ class TestServiceWebProxy(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.cli_delete(base_path) self.cli_commit() + # always forward to base class + super().tearDown() def test_01_basic_proxy(self): default_cache = '100' @@ -315,4 +317,4 @@ class TestServiceWebProxy(VyOSUnitTestSHIM.TestCase): self.assertTrue(process_named_running(PROCESS_NAME)) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_system_acceleration_qat.py b/smoketest/scripts/cli/test_system_acceleration_qat.py index 9e60bb211..744322b4a 100755 --- a/smoketest/scripts/cli/test_system_acceleration_qat.py +++ b/smoketest/scripts/cli/test_system_acceleration_qat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2021 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -26,6 +26,8 @@ class TestIntelQAT(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.cli_delete(base_path) self.cli_commit() + # always forward to base class + super().tearDown() def test_simple_unsupported(self): # Check if configuration script is in place and that the config script @@ -40,4 +42,4 @@ class TestIntelQAT(VyOSUnitTestSHIM.TestCase): self.cli_commit() if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_system_conntrack.py b/smoketest/scripts/cli/test_system_conntrack.py index 72deb7525..e84ad3644 100755 --- a/smoketest/scripts/cli/test_system_conntrack.py +++ b/smoketest/scripts/cli/test_system_conntrack.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,13 +14,19 @@ # 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.firewall import find_nftables_rule -from vyos.utils.file import read_file, read_json +from vyos.utils.file import read_file +from vyos.utils.file import read_json +from vyos.utils.process import cmd +from vyos.utils.system import sysctl_read +from vyos.xml_ref import default_value base_path = ['system', 'conntrack'] @@ -31,6 +37,28 @@ def get_sysctl(parameter): def get_logger_config(): return read_json('/run/vyos-conntrack-logger.conf') + +def chain_priority_conntrack_compatible(table, chain, chain_type, hook): + # Conntrack hooks into nftables at priority -200 + # Verify that base chain priority is a number greater than -200 (lower priority) + # Priority must be lower than conntrack in order to read or update conntrack entries + + chain_contents = cmd(f'sudo nft list chain {table} {chain}') + chain_search = re.search( + rf'type {chain_type} hook {hook} priority (-*\d+)\;', + chain_contents, + ) + + if chain_search is None: + return False + + chain_priority = int(chain_search.group(1)) + + if chain_priority <= -200: + return False + + return True + class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): @@ -43,6 +71,8 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.cli_delete(base_path) self.cli_commit() + # always forward to base class + super().tearDown() def test_conntrack_options(self): conntrack_config = { @@ -168,8 +198,8 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): self.assertTrue(find_nftables_rule('ip vyos_conntrack', 'VYOS_CT_HELPER', [rule]) == None) def test_conntrack_hash_size(self): - hash_size = '65536' - hash_size_default = '32768' + hash_size = '8192' + hash_size_default = default_value(base_path + ['hash-size']) self.cli_set(base_path + ['hash-size', hash_size]) @@ -178,7 +208,7 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): # verify new configuration - only effective after reboot, but # a valid config file is sufficient - tmp = read_file('/etc/modprobe.d/vyatta_nf_conntrack.conf') + tmp = sysctl_read(['net', 'netfilter', 'nf_conntrack_buckets']) self.assertIn(hash_size, tmp) # Test default value by deleting the configuration @@ -189,12 +219,14 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): # verify new configuration - only effective after reboot, but # a valid config file is sufficient - tmp = read_file('/etc/modprobe.d/vyatta_nf_conntrack.conf') + tmp = sysctl_read(['net', 'netfilter', 'nf_conntrack_buckets']) self.assertIn(hash_size_default, tmp) def test_conntrack_ignore(self): address_group = 'conntracktest' address_group_member = '192.168.0.1' + port_single = '53' + ports_multi = '500,4500' ipv6_address_group = 'conntracktest6' ipv6_address_group_member = 'dead:beef::1' @@ -211,6 +243,14 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '2', 'destination', 'group', 'address-group', address_group]) self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '2', 'protocol', 'all']) + self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '3', 'source', 'address', '192.0.2.1']) + self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '3', 'destination', 'port', ports_multi]) + self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '3', 'protocol', 'udp']) + + self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '4', 'source', 'address', '192.0.2.1']) + self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '4', 'destination', 'port', port_single]) + self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '4', 'protocol', 'udp']) + self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '11', 'source', 'address', 'fe80::1']) self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '11', 'destination', 'address', 'fe80::2']) self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '11', 'destination', 'port', '22']) @@ -226,7 +266,9 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): nftables_search = [ ['ip saddr 192.0.2.1', 'ip daddr 192.0.2.2', 'tcp dport 22', 'tcp flags & syn == syn', 'notrack'], - ['ip saddr 192.0.2.1', 'ip daddr @A_conntracktest', 'notrack'] + ['ip saddr 192.0.2.1', 'ip daddr @A_conntracktest', 'notrack'], + ['ip saddr 192.0.2.1', 'udp dport { 500, 4500 }', 'notrack'], + ['ip saddr 192.0.2.1', 'udp dport 53', 'notrack'] ] nftables6_search = [ @@ -241,6 +283,43 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): self.cli_delete(['firewall']) def test_conntrack_timeout_custom(self): + # No timeout rules configured yet, so there should be no VYOS_CT_TIMEOUT chain or timeout base chains + # Timeout base chains MUST have priority higher than -200 because conntrack hooks at -200 + prerouting_timeout_chain = [ + ['chain PREROUTING_CT_TIMEOUT {'], + ['type filter hook prerouting priority'], + ['jump VYOS_CT_TIMEOUT'], + ] + output_timeout_chain = [ + ['chain OUTPUT_CT_TIMEOUT {'], + ['type filter hook output priority'], + ['jump VYOS_CT_TIMEOUT'], + ] + + # None of these chains should exist yet + self.verify_nftables_chain_exists( + 'ip vyos_conntrack', 'VYOS_CT_TIMEOUT', inverse=True + ) + + self.verify_nftables_chain_exists( + 'ip6 vyos_conntrack', 'VYOS_CT_TIMEOUT', inverse=True + ) + + self.verify_nftables_chain_exists( + 'ip vyos_conntrack', 'PREROUTING_CT_TIMEOUT', inverse=True + ) + + self.verify_nftables_chain_exists( + 'ip6 vyos_conntrack', 'PREROUTING_CT_TIMEOUT', inverse=True + ) + + self.verify_nftables_chain_exists( + 'ip vyos_conntrack', 'OUTPUT_CT_TIMEOUT', inverse=True + ) + + self.verify_nftables_chain_exists( + 'ip6 vyos_conntrack', 'OUTPUT_CT_TIMEOUT', inverse=True + ) self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '1', 'source', 'address', '192.0.2.1']) self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '1', 'destination', 'address', '192.0.2.2']) @@ -253,6 +332,50 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '2', 'source', 'address', '198.51.100.1']) self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '2', 'protocol', 'udp', 'unreplied', '55']) + self.cli_commit() + + # We now have IPv4 custom timeout rules, so only the IPv4 table should contain the chains + self.verify_nftables_chain_exists('ip vyos_conntrack', 'VYOS_CT_TIMEOUT') + + self.verify_nftables_chain_exists( + 'ip6 vyos_conntrack', 'VYOS_CT_TIMEOUT', inverse=True + ) + + self.verify_nftables_chain_exists('ip vyos_conntrack', 'PREROUTING_CT_TIMEOUT') + + self.verify_nftables_chain_exists( + 'ip6 vyos_conntrack', 'PREROUTING_CT_TIMEOUT', inverse=True + ) + + self.verify_nftables_chain_exists('ip vyos_conntrack', 'OUTPUT_CT_TIMEOUT') + + self.verify_nftables_chain_exists( + 'ip6 vyos_conntrack', 'OUTPUT_CT_TIMEOUT', inverse=True + ) + + self.verify_nftables_chain( + prerouting_timeout_chain, 'ip vyos_conntrack', 'PREROUTING_CT_TIMEOUT' + ) + + self.verify_nftables_chain( + output_timeout_chain, 'ip vyos_conntrack', 'OUTPUT_CT_TIMEOUT' + ) + + # Verify that IPv4 base chain priority is a number greater than -200 + if not chain_priority_conntrack_compatible( + 'ip vyos_conntrack', 'PREROUTING_CT_TIMEOUT', 'filter', 'prerouting' + ): + self.fail( + 'PREROUTING_CT_TIMEOUT base chain must have priority > -200 to read and update conntrack entries' + ) + + if not chain_priority_conntrack_compatible( + 'ip vyos_conntrack', 'OUTPUT_CT_TIMEOUT', 'filter', 'output' + ): + self.fail( + 'OUTPUT_CT_TIMEOUT base chain must have priority > -200 to read and update conntrack entries' + ) + self.cli_set(base_path + ['timeout', 'custom', 'ipv6', 'rule', '1', 'source', 'address', '2001:db8::1']) self.cli_set(base_path + ['timeout', 'custom', 'ipv6', 'rule', '1', 'inbound-interface', 'eth2']) self.cli_set(base_path + ['timeout', 'custom', 'ipv6', 'rule', '1', 'protocol', 'tcp', 'time-wait', '22']) @@ -260,6 +383,47 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): self.cli_commit() + # Now we have both IPv4 and IPv6 custom timeout rules + # The chains should exist in both the IPv4 and IPv6 tables + self.verify_nftables_chain_exists('ip vyos_conntrack', 'VYOS_CT_TIMEOUT') + self.verify_nftables_chain_exists('ip6 vyos_conntrack', 'VYOS_CT_TIMEOUT') + self.verify_nftables_chain_exists('ip vyos_conntrack', 'PREROUTING_CT_TIMEOUT') + self.verify_nftables_chain_exists('ip6 vyos_conntrack', 'PREROUTING_CT_TIMEOUT') + self.verify_nftables_chain_exists('ip vyos_conntrack', 'OUTPUT_CT_TIMEOUT') + self.verify_nftables_chain_exists('ip6 vyos_conntrack', 'OUTPUT_CT_TIMEOUT') + + self.verify_nftables_chain( + prerouting_timeout_chain, 'ip vyos_conntrack', 'PREROUTING_CT_TIMEOUT' + ) + + self.verify_nftables_chain( + prerouting_timeout_chain, 'ip6 vyos_conntrack', 'PREROUTING_CT_TIMEOUT' + ) + + self.verify_nftables_chain( + output_timeout_chain, 'ip vyos_conntrack', 'OUTPUT_CT_TIMEOUT' + ) + + self.verify_nftables_chain( + output_timeout_chain, 'ip6 vyos_conntrack', 'OUTPUT_CT_TIMEOUT' + ) + + # Verify that IPv6 base chain priority is a number greater than -200 + if not chain_priority_conntrack_compatible( + 'ip6 vyos_conntrack', 'PREROUTING_CT_TIMEOUT', 'filter', 'prerouting' + ): + self.fail( + 'PREROUTING_CT_TIMEOUT base chain must have priority > -200 to read and update conntrack entries' + ) + + if not chain_priority_conntrack_compatible( + 'ip6 vyos_conntrack', 'OUTPUT_CT_TIMEOUT', 'filter', 'output' + ): + self.fail( + 'OUTPUT_CT_TIMEOUT base chain must have priority > -200 to read and update conntrack entries' + ) + + # Verify rules are correctly output in nftables nftables_search = [ ['ct timeout ct-timeout-1 {'], ['protocol tcp'], @@ -283,6 +447,65 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip vyos_conntrack') self.verify_nftables(nftables6_search, 'ip6 vyos_conntrack') + # remove IPv4 custom timeout rules and verify only the IPv6 chains still exist + self.cli_delete(base_path + ['timeout', 'custom', 'ipv4']) + self.cli_commit() + + # only the IPv6 chains should remain + self.verify_nftables_chain_exists( + 'ip vyos_conntrack', 'VYOS_CT_TIMEOUT', inverse=True + ) + + self.verify_nftables_chain_exists('ip6 vyos_conntrack', 'VYOS_CT_TIMEOUT') + + self.verify_nftables_chain_exists( + 'ip vyos_conntrack', 'PREROUTING_CT_TIMEOUT', inverse=True + ) + + self.verify_nftables_chain_exists('ip6 vyos_conntrack', 'PREROUTING_CT_TIMEOUT') + + self.verify_nftables_chain_exists( + 'ip vyos_conntrack', 'OUTPUT_CT_TIMEOUT', inverse=True + ) + + self.verify_nftables_chain_exists('ip6 vyos_conntrack', 'OUTPUT_CT_TIMEOUT') + + self.verify_nftables_chain( + prerouting_timeout_chain, 'ip6 vyos_conntrack', 'PREROUTING_CT_TIMEOUT' + ) + + self.verify_nftables_chain( + output_timeout_chain, 'ip6 vyos_conntrack', 'OUTPUT_CT_TIMEOUT' + ) + + # remove custom timeout config and verify all chains are gone once again + self.cli_delete(base_path + ['timeout']) + self.cli_commit() + + self.verify_nftables_chain_exists( + 'ip vyos_conntrack', 'VYOS_CT_TIMEOUT', inverse=True + ) + + self.verify_nftables_chain_exists( + 'ip6 vyos_conntrack', 'VYOS_CT_TIMEOUT', inverse=True + ) + + self.verify_nftables_chain_exists( + 'ip vyos_conntrack', 'PREROUTING_CT_TIMEOUT', inverse=True + ) + + self.verify_nftables_chain_exists( + 'ip6 vyos_conntrack', 'PREROUTING_CT_TIMEOUT', inverse=True + ) + + self.verify_nftables_chain_exists( + 'ip vyos_conntrack', 'OUTPUT_CT_TIMEOUT', inverse=True + ) + + self.verify_nftables_chain_exists( + 'ip6 vyos_conntrack', 'OUTPUT_CT_TIMEOUT', inverse=True + ) + self.cli_delete(['firewall']) def test_conntrack_log(self): @@ -315,4 +538,4 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_system_console.py b/smoketest/scripts/cli/test_system_console.py new file mode 100755 index 000000000..388e9236f --- /dev/null +++ b/smoketest/scripts/cli/test_system_console.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +# +# Copyright (C) VyOS Inc. +# +# 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.system import disk +from vyos.system.grub import CFG_VYOS_VARS +from vyos.system.grub import vars_read +from vyos.xml_ref import default_value + +base_path = ['system', 'console'] +serial_console = 'ttyS0' +default_speed = default_value(base_path + ['device', serial_console, 'speed']) + +def get_grub_vars() -> dict: + root_dir = disk.find_persistence() + vars_file: str = f'{root_dir}/{CFG_VYOS_VARS}' + vars_current: dict[str, str] = vars_read(vars_file) + return vars_current + +class TestSystemConsole(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(TestSystemConsole, 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_commit(cls) + + def tearDown(self): + self.cli_delete(base_path) + self.cli_commit() + # always forward to base class + super().tearDown() + + def test_multiple_kernel_consoles(self): + self.cli_set(base_path + ['device', 'ttyS1', 'kernel']) + self.cli_set(base_path + ['device', 'ttyS2', 'kernel']) + + # Only one console can have 'kernel' + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + def test_fbcon_and_serial_con_switch(self): + if not os.path.exists('/tmp/vyos.smoketests.hint'): + self.skipTest('Not running under VyOS CI/CD QEMU environment!') + + grub_vars = get_grub_vars() + # we have deleted the CLI config in tearDown() so the default is now + # the framebuffer console at tty0 + self.assertEqual(grub_vars['console_type'], 'tty') + + self.cli_set(base_path + ['device', serial_console, 'kernel']) + self.cli_commit() + + grub_vars = get_grub_vars() + # We moved the Kernel boot console to ttyS0 + self.assertEqual(grub_vars['console_type'], serial_console[:-1]) + self.assertEqual(grub_vars['console_num'], serial_console[-1]) + self.assertEqual(grub_vars['console_speed'], default_speed) + + self.cli_delete(base_path) + self.cli_commit() + + # We moved back to tty as Kernel boot console + grub_vars = get_grub_vars() + self.assertEqual(grub_vars['console_type'], 'tty') + +if __name__ == '__main__': + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_system_flow-accounting.py b/smoketest/scripts/cli/test_system_flow-accounting.py index 9d7942789..a5ab9b1fd 100755 --- a/smoketest/scripts/cli/test_system_flow-accounting.py +++ b/smoketest/scripts/cli/test_system_flow-accounting.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -20,17 +20,53 @@ from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section -from vyos.template import bracketize_ipv6 +from vyos.utils.kernel import is_module_loaded +from vyos.utils.kernel import get_module_data from vyos.utils.process import cmd -from vyos.utils.process import process_named_running -from vyos.utils.file import read_file -PROCESS_NAME = 'uacctd' +module_name = 'ipt_NETFLOW' base_path = ['system', 'flow-accounting'] -uacctd_conf = '/run/pmacct/uacctd.conf' class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase): + + def _get_iptables_watched_interfaces(self, command, table, chain, column_name): + iptables_command = f'{command} -vn -t {table} -L {chain}' + data = cmd(iptables_command, message='Failed to get flows list') + data = data.splitlines() + self.assertGreaterEqual( + len(data), 2, "Unexpected output of {command}, should be at least two lines" + ) + column_index = data[1].split().index(column_name) + interfaces = [ + line.split()[column_index] for line in data[2:] if 'NETFLOW' in line + ] + return interfaces + + def _get_iptables_watched_ingress_interfaces(self, command): + return self._get_iptables_watched_interfaces(command, 'raw', 'PREROUTING', 'in') + + def _get_iptables_watched_egress_interfaces(self, command): + return self._get_iptables_watched_interfaces( + command, 'mangle', 'POSTROUTING', 'out' + ) + + def _assert_ingress_interfaces(self, interfaces): + for command in 'iptables', 'ip6tables': + self.assertEqual( + set(self._get_iptables_watched_ingress_interfaces(command)), + set(interfaces), + command, + ) + + def _assert_egress_interfaces(self, interfaces): + for command in 'iptables', 'ip6tables': + self.assertEqual( + set(self._get_iptables_watched_egress_interfaces(command)), + set(interfaces), + command, + ) + @classmethod def setUpClass(cls): super(TestSystemFlowAccounting, cls).setUpClass() @@ -41,106 +77,85 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase): def tearDown(self): # after service removal process must no longer run - self.assertTrue(process_named_running(PROCESS_NAME)) + self.assertTrue(is_module_loaded(module_name)) self.cli_delete(base_path) self.cli_commit() # after service removal process must no longer run - self.assertFalse(process_named_running(PROCESS_NAME)) + self.assertFalse(is_module_loaded(module_name)) + self._assert_ingress_interfaces([]) + self._assert_egress_interfaces([]) + # always forward to base class + super().tearDown() def test_basic(self): - buffer_size = '5' # MiB - syslog = 'all' - - self.cli_set(base_path + ['buffer-size', buffer_size]) - self.cli_set(base_path + ['syslog-facility', syslog]) + engine_id = '33' + self.cli_set(base_path + ['netflow', 'engine-id', engine_id]) # 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]) + self.cli_set(base_path + ['netflow', 'interface', interface]) - # commit changes + # You need to configure at least one NetFlow server + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + netflow_server = '11.22.33.44' + self.cli_set(base_path + ['netflow', 'server', netflow_server]) + + # commit changes, this time should work self.cli_commit() # verify configuration - nftables_output = cmd('sudo nft list chain raw VYOS_PREROUTING_HOOK').splitlines() - for interface in Section.interfaces('ethernet'): - rule_found = False - ifname_search = f'iifname "{interface}"' - - for nftables_line in nftables_output: - if 'FLOW_ACCOUNTING_RULE' in nftables_line and ifname_search in nftables_line: - self.assertIn('group 2', nftables_line) - self.assertIn('snaplen 128', nftables_line) - self.assertIn('queue-threshold 100', nftables_line) - rule_found = True - break - - self.assertTrue(rule_found) - - uacctd = read_file(uacctd_conf) - # circular queue size - buffer_size - tmp = int(buffer_size) *1024 *1024 - self.assertIn(f'plugin_pipe_size: {tmp}', uacctd) - # transfer buffer size - recommended value from pmacct developers 1/1000 of pipe size - tmp = int(buffer_size) *1024 *1024 - # do an integer division - tmp //= 1000 - self.assertIn(f'plugin_buffer_size: {tmp}', uacctd) - - # when 'disable-imt' is not configured on the CLI it must be present - self.assertIn(f'imt_path: /tmp/uacctd.pipe', uacctd) - self.assertIn(f'imt_mem_pools_number: 169', uacctd) - self.assertIn(f'syslog: {syslog}', uacctd) - self.assertIn(f'plugins: memory', uacctd) + self._assert_ingress_interfaces(Section.interfaces('ethernet')) + self._assert_egress_interfaces([]) + + module_data = get_module_data(module_name) + self.assertEqual(engine_id, module_data['parameters']['engine_id']) + def test_netflow(self): engine_id = '33' max_flows = '667' - sampling_rate = '100' - source_address = '192.0.2.1' dummy_if = 'dum3842' agent_address = '192.0.2.10' version = '10' - tmo_expiry = '120' - tmo_flow = '1200' - tmo_icmp = '60' - tmo_max = '50000' - tmo_tcp_fin = '100' - tmo_tcp_generic = '120' - tmo_tcp_rst = '99' - tmo_udp = '10' + active_timeout = '900' + inactive_timeout = '30' + source_ipv4_address = '192.0.2.1' + source_ipv6_address = '2001:db8::ab' netflow_server = { - '11.22.33.44' : { }, - '55.66.77.88' : { 'port' : '6000' }, - '2001:db8::1' : { }, + '11.22.33.44': {}, + '55.66.77.88': {'port': '6000'}, + '100.12.14.1': {'source-interface': dummy_if}, + '203.0.113.21': {'port': '3000', 'source-address': source_ipv4_address}, + '2001:db8::1': {'source-address': source_ipv6_address}, } - - self.cli_set(['interfaces', 'dummy', dummy_if, 'address', agent_address + '/32']) - self.cli_set(['interfaces', 'dummy', dummy_if, 'address', source_address + '/32']) + # ipt_NETFLOW sorts destinations by IP + expected_destination = '11.22.33.44:2055,55.66.77.88:6000,100.12.14.1:2055%dum3842,203.0.113.21:3000@192.0.2.1,[2001:db8::1]:2055@2001:db8::ab' + + self.cli_set( + ['interfaces', 'dummy', dummy_if, 'address', agent_address + '/32'] + ) + self.cli_set( + ['interfaces', 'dummy', dummy_if, 'address', source_ipv4_address + '/32'] + ) + self.cli_set( + ['interfaces', 'dummy', dummy_if, 'address', source_ipv6_address + '/128'] + ) for interface in Section.interfaces('ethernet'): - self.cli_set(base_path + ['interface', interface]) + self.cli_set(base_path + ['netflow', 'interface', interface]) self.cli_set(base_path + ['netflow', 'engine-id', engine_id]) self.cli_set(base_path + ['netflow', 'max-flows', max_flows]) - self.cli_set(base_path + ['netflow', 'sampling-rate', sampling_rate]) - self.cli_set(base_path + ['netflow', 'source-address', source_address]) self.cli_set(base_path + ['netflow', 'version', version]) - - # timeouts - self.cli_set(base_path + ['netflow', 'timeout', 'expiry-interval', tmo_expiry]) - self.cli_set(base_path + ['netflow', 'timeout', 'flow-generic', tmo_flow]) - self.cli_set(base_path + ['netflow', 'timeout', 'icmp', tmo_icmp]) - self.cli_set(base_path + ['netflow', 'timeout', 'max-active-life', tmo_max]) - self.cli_set(base_path + ['netflow', 'timeout', 'tcp-fin', tmo_tcp_fin]) - self.cli_set(base_path + ['netflow', 'timeout', 'tcp-generic', tmo_tcp_generic]) - self.cli_set(base_path + ['netflow', 'timeout', 'tcp-rst', tmo_tcp_rst]) - self.cli_set(base_path + ['netflow', 'timeout', 'udp', tmo_udp]) + self.cli_set(base_path + ['netflow', 'active-timeout', active_timeout]) + self.cli_set(base_path + ['netflow', 'inactive-timeout', inactive_timeout]) # You need to configure at least one netflow server with self.assertRaises(ConfigSessionError): @@ -150,41 +165,134 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['netflow', 'server', server]) if 'port' in server_config: self.cli_set(base_path + ['netflow', 'server', server, 'port', server_config['port']]) + if 'source-address' in server_config: + self.cli_set( + base_path + + [ + 'netflow', + 'server', + server, + 'source-address', + server_config['source-address'], + ] + ) + if 'source-interface' in server_config: + self.cli_set( + base_path + + [ + 'netflow', + 'server', + server, + 'source-interface', + server_config['source-interface'], + ] + ) # commit changes self.cli_commit() - uacctd = read_file(uacctd_conf) + module_data = get_module_data(module_name) - tmp = [] - for server, server_config in netflow_server.items(): - tmp_srv = server - tmp_srv = tmp_srv.replace('.', '-') - tmp_srv = tmp_srv.replace(':', '-') - tmp.append(f'nfprobe[nf_{tmp_srv}]') - tmp.append('memory') - self.assertIn('plugins: ' + ','.join(tmp), uacctd) + self.assertEqual(engine_id, module_data['parameters']['engine_id']) + self.assertEqual(max_flows, module_data['parameters']['maxflows']) + self.assertEqual(expected_destination, module_data['parameters']['destination']) + self.assertEqual(version, module_data['parameters']['protocol']) + self.assertEqual(active_timeout, module_data['parameters']['active_timeout']) + self.assertEqual( + inactive_timeout, module_data['parameters']['inactive_timeout'] + ) - for server, server_config in netflow_server.items(): - tmp_srv = server - tmp_srv = tmp_srv.replace('.', '-') - tmp_srv = tmp_srv.replace(':', '-') - - self.assertIn(f'nfprobe_engine[nf_{tmp_srv}]: {engine_id}', uacctd) - self.assertIn(f'nfprobe_maxflows[nf_{tmp_srv}]: {max_flows}', uacctd) - self.assertIn(f'sampling_rate[nf_{tmp_srv}]: {sampling_rate}', uacctd) - self.assertIn(f'nfprobe_source_ip[nf_{tmp_srv}]: {source_address}', uacctd) - self.assertIn(f'nfprobe_version[nf_{tmp_srv}]: {version}', uacctd) + # Test module reload with new parameters + engine_id = '73' + self.cli_set(base_path + ['netflow', 'engine-id', engine_id]) + self.cli_commit() - if 'port' in server_config: - self.assertIn(f'nfprobe_receiver[nf_{tmp_srv}]: {bracketize_ipv6(server)}', uacctd) - else: - self.assertIn(f'nfprobe_receiver[nf_{tmp_srv}]: {bracketize_ipv6(server)}:2055', uacctd) + module_data = get_module_data(module_name) - 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.assertEqual(engine_id, module_data['parameters']['engine_id']) self.cli_delete(['interfaces', 'dummy', dummy_if]) + def test_iptables(self): + netflow_server = '11.22.33.44' + self.cli_set(base_path + ['netflow', 'server', netflow_server]) + + dummy_ifs = [ + 'dum4000', + 'dum4001', + 'dum4002', + 'dum4003', + ] + + self.cli_set(['interfaces', 'dummy', dummy_ifs[0], 'address', '192.0.2.100/32']) + self.cli_set(['interfaces', 'dummy', dummy_ifs[1], 'address', '192.0.2.101/32']) + self.cli_set(['interfaces', 'dummy', dummy_ifs[2], 'address', '192.0.2.102/32']) + self.cli_set(['interfaces', 'dummy', dummy_ifs[3], 'address', '192.0.2.103/32']) + + # * three interfaces + for i in range(3): + self.cli_set(base_path + ['netflow', 'interface', dummy_ifs[i]]) + self.cli_commit() + self._assert_ingress_interfaces(dummy_ifs[0:3]) + self._assert_egress_interfaces([]) + + # * Then delete one + self.cli_delete(base_path + ['netflow', 'interface', dummy_ifs[1]]) + self.cli_commit() + self._assert_ingress_interfaces([dummy_ifs[0], dummy_ifs[2]]) + self._assert_egress_interfaces([]) + + # * Then add one + self.cli_set(base_path + ['netflow', 'interface', dummy_ifs[3]]) + self.cli_commit() + self._assert_ingress_interfaces([dummy_ifs[0], dummy_ifs[2], dummy_ifs[3]]) + self._assert_egress_interfaces([]) + + # * enable egress + self.cli_set(base_path + ['enable-egress']) + self.cli_commit() + self._assert_ingress_interfaces([dummy_ifs[0], dummy_ifs[2], dummy_ifs[3]]) + self._assert_egress_interfaces([dummy_ifs[0], dummy_ifs[2], dummy_ifs[3]]) + + def test_sampler(self): + # Separate test because if --enable-sampler is not given to configure of + # ipt_NETFLOW this parameter is not available + sampling_rate = '100' + self.cli_set(base_path + ['netflow', 'sampling-rate', sampling_rate]) + + for interface in Section.interfaces('ethernet'): + self.cli_set(base_path + ['netflow', 'interface', interface]) + + netflow_server = '11.22.33.44' + self.cli_set(base_path + ['netflow', 'server', netflow_server]) + + # commit changes, this time should work + self.cli_commit() + + module_data = get_module_data(module_name) + + if 'sampler' not in module_data['parameters']: + self.skipTest("ipt_NETFLOW has no sampler parameter") + + self.assertEqual( + f'random:{sampling_rate}', module_data['parameters']['sampler'] + ) + + + def test_netflow_v5(self): + version = '5' + netflow_server = '203.0.113.27' + + for interface in Section.interfaces('ethernet'): + self.cli_set(base_path + ['netflow', 'interface', interface]) + + self.cli_set(base_path + ['netflow', 'version', version]) + self.cli_set(base_path + ['netflow', 'server', netflow_server]) + + # commit changes + self.cli_commit() + + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_system_frr.py b/smoketest/scripts/cli/test_system_frr.py index a2ce58bf6..ea280dd27 100755 --- a/smoketest/scripts/cli/test_system_frr.py +++ b/smoketest/scripts/cli/test_system_frr.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,6 +19,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.utils.file import read_file +from vyos.xml_ref import default_value config_file = '/etc/frr/daemons' base_path = ['system', 'frr'] @@ -51,6 +52,8 @@ class TestSystemFRR(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.cli_delete(base_path) self.cli_commit() + # always forward to base class + super().tearDown() def test_frr_snmp_multipledaemons(self): # test SNMP integration for multiple daemons @@ -148,7 +151,34 @@ class TestSystemFRR(VyOSUnitTestSHIM.TestCase): self.assertTrue(bmp_enabled) self.assertTrue(snmp_enabled) + def test_frr_profile_add_remove(self): + default_profile = default_value(base_path + ['profile']) + + # test add profile + frr_profiles = ['traditional', 'datacenter'] + for profile in frr_profiles: + # set the profile + self.cli_set(base_path + ['profile', profile]) + self.cli_commit() + # read the config file and check content + self.assertIn(f'frr_profile="{profile}"', read_file(config_file)) + # read the frr.conf file and check content + frrconfig = self.getFRRconfig() + self.assertIn(f'frr defaults {profile}', frrconfig) + + # test remove profile + self.cli_delete(base_path) + self.cli_commit() + + # read the config file and check content + self.assertIn(f'frr_profile="{default_profile}"', read_file(config_file)) + + # read the frr.conf file and check content + frrconfig = self.getFRRconfig() + self.assertIn(f'frr defaults {default_profile}', frrconfig) + def test_frr_file_descriptors(self): + default_descriptors = default_value(base_path + ['descriptors']) file_descriptors = '4096' self.cli_set(base_path + ['descriptors', file_descriptors]) @@ -158,5 +188,37 @@ class TestSystemFRR(VyOSUnitTestSHIM.TestCase): daemons_config = read_file(config_file) self.assertIn(f'MAX_FDS={file_descriptors}', daemons_config) + # test remove of descriptors + self.cli_delete(base_path) + self.cli_commit() + + # read the config file and check content + daemons_config = read_file(config_file) + self.assertIn(f'MAX_FDS={default_descriptors}', daemons_config) + + def test_frr_watchfrr_timeout(self): + default_watchfrr_timeout = default_value(base_path + ['watchfrr-timeout']) + watchfrr_timeout = '120' + + self.cli_set(base_path + ['watchfrr-timeout', watchfrr_timeout]) + self.cli_commit() + + # read the config file and check content + daemons_config = read_file(config_file) + self.assertIn( + f'watchfrr_options="--timeout={watchfrr_timeout}"', daemons_config + ) + + # test remove of watchfrr-timeout + self.cli_delete(base_path) + self.cli_commit() + + # read the config file and check content + daemons_config = read_file(config_file) + self.assertIn( + f'watchfrr_options="--timeout={default_watchfrr_timeout}"', daemons_config + ) + + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_system_ip.py b/smoketest/scripts/cli/test_system_ip.py index 5b6ef2046..43ae4e008 100755 --- a/smoketest/scripts/cli/test_system_ip.py +++ b/smoketest/scripts/cli/test_system_ip.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,42 +34,46 @@ class TestSystemIP(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.cli_delete(base_path) self.cli_commit() + # always forward to base class + super().tearDown() def test_system_ip_forwarding(self): # Test if IPv4 forwarding can be disabled globally, default is '1' # which means forwarding enabled - self.assertEqual(sysctl_read('net.ipv4.conf.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.assertEqual(sysctl_read(['net', 'ipv4', 'conf', 'all', 'forwarding']), '0') + frrconfig = self.getFRRconfig() self.assertIn('no ip forwarding', frrconfig) 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.assertEqual(sysctl_read(['net', 'ipv4', 'conf', 'all', 'forwarding']), '1') + frrconfig = self.getFRRconfig() self.assertNotIn('no ip forwarding', frrconfig) def test_system_ip_multipath(self): # Test IPv4 multipathing options, options default to off -> '0' - self.assertEqual(sysctl_read('net.ipv4.fib_multipath_use_neigh'), '0') - self.assertEqual(sysctl_read('net.ipv4.fib_multipath_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(sysctl_read('net.ipv4.fib_multipath_use_neigh'), '1') - self.assertEqual(sysctl_read('net.ipv4.fib_multipath_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): 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)) + 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)) _verify_gc_thres(cli_default) @@ -79,17 +83,18 @@ class TestSystemIP(VyOSUnitTestSHIM.TestCase): _verify_gc_thres(size) def test_system_ip_protocol_route_map(self): - protocols = ['any', 'babel', 'bgp', 'connected', 'eigrp', 'isis', - 'kernel', 'ospf', 'rip', 'static', 'table'] + protocols = ['any', 'babel', 'bgp', 'eigrp', 'isis', 'ospf', 'rip', 'static'] + + rule_num = '10' for protocol in protocols: - self.cli_set(['policy', 'route-map', f'route-map-{protocol}', 'rule', '10', 'action', 'permit']) + self.cli_set(['policy', 'route-map', f'route-map-{protocol}', 'rule', rule_num, 'action', 'permit']) self.cli_set(base_path + ['protocol', protocol, 'route-map', f'route-map-{protocol}']) self.cli_commit() # Verify route-map properly applied to FRR - frrconfig = self.getFRRconfig('ip protocol', end='') + frrconfig = self.getFRRconfig('ip protocol', end_marker='', stop_section='^end') for protocol in protocols: self.assertIn(f'ip protocol {protocol} route-map route-map-{protocol}', frrconfig) @@ -100,7 +105,7 @@ class TestSystemIP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify route-map properly applied to FRR - frrconfig = self.getFRRconfig('ip protocol', end='') + frrconfig = self.getFRRconfig('ip protocol', stop_section='^end') self.assertNotIn(f'ip protocol', frrconfig) def test_system_ip_protocol_non_existing_route_map(self): @@ -119,14 +124,35 @@ 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='') + frrconfig = self.getFRRconfig() 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='') + frrconfig = self.getFRRconfig() self.assertNotIn(f'no ip nht resolve-via-default', frrconfig) + def test_system_ip_import_table(self): + table_num = '100' + distance = '200' + route_map_in = 'foo-map-in' + self.cli_set(['policy', 'route-map', route_map_in, 'rule', '10', 'action', 'permit']) + self.cli_set(base_path + ['import-table', table_num, 'distance', distance]) + self.cli_set(base_path + ['import-table', table_num, 'route-map', route_map_in]) + + self.cli_commit() + # Verify CLI config applied to FRR + frrconfig = self.getFRRconfig() + self.assertIn(f'ip import-table {table_num} distance {distance} route-map {route_map_in}', frrconfig) + + self.cli_delete(['policy', 'route-map', route_map_in]) + + self.cli_delete(base_path + ['import-table']) + self.cli_commit() + # Verify CLI config removed to FRR + frrconfig = self.getFRRconfig() + self.assertNotIn(f'ip import-table {table_num} distance {distance}', frrconfig) + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_system_ipv6.py b/smoketest/scripts/cli/test_system_ipv6.py index 26f281bb4..eacf833ae 100755 --- a/smoketest/scripts/cli/test_system_ipv6.py +++ b/smoketest/scripts/cli/test_system_ipv6.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -35,27 +35,29 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.cli_delete(base_path) self.cli_commit() + # always forward to base class + super().tearDown() def test_system_ipv6_forwarding(self): # Test if IPv6 forwarding can be disabled globally, default is '1' # which means forwearding enabled - self.assertEqual(sysctl_read('net.ipv6.conf.all.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.assertEqual(sysctl_read(['net', 'ipv6', 'conf', 'all', 'forwarding']), '0') + frrconfig = self.getFRRconfig() self.assertIn('no ipv6 forwarding', frrconfig) 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.assertEqual(sysctl_read(['net', 'ipv6', 'conf', 'all', 'forwarding']), '1') + frrconfig = self.getFRRconfig() self.assertNotIn('no ipv6 forwarding', frrconfig) def test_system_ipv6_strict_dad(self): # This defaults to 1 - self.assertEqual(sysctl_read('net.ipv6.conf.all.accept_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 :) @@ -63,11 +65,11 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify configuration file - self.assertEqual(sysctl_read('net.ipv6.conf.all.accept_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(sysctl_read('net.ipv6.fib_multipath_hash_policy'), '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 :) @@ -75,7 +77,7 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify configuration file - self.assertEqual(sysctl_read('net.ipv6.fib_multipath_hash_policy'), '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 @@ -83,9 +85,9 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase): cli_default = int(default_value(base_path + ['neighbor', 'table-size'])) 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)) + 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) @@ -95,8 +97,7 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase): _verify_gc_thres(size) def test_system_ipv6_protocol_route_map(self): - protocols = ['any', 'babel', 'bgp', 'connected', 'isis', - 'kernel', 'ospfv3', 'ripng', 'static', 'table'] + protocols = ['any', 'babel', 'bgp', 'isis', 'ospfv3', 'ripng', 'static'] for protocol in protocols: route_map = 'route-map-' + protocol.replace('ospfv3', 'ospf6') @@ -107,7 +108,7 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify route-map properly applied to FRR - frrconfig = self.getFRRconfig('ipv6 protocol', end='') + frrconfig = self.getFRRconfig('ipv6 protocol', end_marker='', stop_section='^end') for protocol in protocols: # VyOS and FRR use a different name for OSPFv3 (IPv6) if protocol == 'ospfv3': @@ -121,7 +122,7 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify route-map properly applied to FRR - frrconfig = self.getFRRconfig('ipv6 protocol', end='') + frrconfig = self.getFRRconfig('ipv6 protocol', stop_section='^end') self.assertNotIn(f'ipv6 protocol', frrconfig) def test_system_ipv6_protocol_non_existing_route_map(self): @@ -140,14 +141,14 @@ 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='') + frrconfig = self.getFRRconfig() 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='') + frrconfig = self.getFRRconfig() self.assertNotIn(f'no ipv6 nht resolve-via-default', frrconfig) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_system_lcd.py b/smoketest/scripts/cli/test_system_lcd.py index fc440ca8a..27f7ede6b 100755 --- a/smoketest/scripts/cli/test_system_lcd.py +++ b/smoketest/scripts/cli/test_system_lcd.py @@ -28,6 +28,8 @@ class TestSystemLCD(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.cli_delete(base_path) self.cli_commit() + # always forward to base class + super().tearDown() def test_system_display(self): # configure some system display @@ -48,4 +50,4 @@ class TestSystemLCD(VyOSUnitTestSHIM.TestCase): self.assertTrue(process_named_running('LCDd')) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py index ed72f378e..7088097c4 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-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -25,16 +25,15 @@ import shutil from base_vyostest_shim import VyOSUnitTestSHIM -from contextlib import redirect_stdout from gzip import GzipFile -from io import StringIO, TextIOWrapper 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 DEFAULT_PASSWORD from vyos.utils.auth import get_current_user +from vyos.utils.auth import get_local_passwd_entries from vyos.utils.process import cmd from vyos.utils.file import read_file from vyos.utils.file import write_file @@ -177,9 +176,11 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): self.cli_commit() # After deletion, a user is not allowed to remain in /etc/passwd - usernames = [x[0] for x in getpwall()] + usernames = [x.pw_name for x in get_local_passwd_entries()] for user in users: self.assertNotIn(user, usernames) + # always forward to base class + super().tearDown() def test_add_linux_system_user(self): # We are not allowed to re-use a username already taken by the Linux @@ -235,14 +236,22 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): self.assertIn(f'{locked_user} P ', tmp) def test_system_login_weak_password_warning(self): + username = weak_passwd_user[0] self.cli_set(base_path + [ - 'user', weak_passwd_user[0], 'authentication', + 'user', username, 'authentication', 'plaintext-password', weak_passwd_user[1] ]) out = self.cli_commit().strip() + self.assertIn(f'WARNING: User "{username}" - The password complexity is too low', out) + + self.cli_set(base_path + [ + 'user', username, 'authentication', + 'plaintext-password', DEFAULT_PASSWORD]) + + out = self.cli_commit().strip() + self.assertIn(f'WARNING: Default password used for user "{username}"', out) - 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): @@ -550,5 +559,34 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): self.cli_commit() self.cli_discard() + def test_pam_nologin(self): + # Testcase for T7443, test if we can login with a non-privileged user + # when there are only 5 minutes left until the system reboots + username = users[0] + password = f'{username}-pSWd-t3st' + + self.cli_set(base_path + ['user', username, 'authentication', 'plaintext-password', password]) + self.cli_commit() + + # 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) + + # Request system reboot in 5 minutes - this will activate pam_nologin.so + # and prevent any login - but we have this disabled, so we must be able + # to login to the router + self.op_mode(['reboot', 'in', '4']) + + # verify login + # Login with proper credentials - after reboot is pending + out, err = self.ssh_send_cmd(ssh_test_command, username, password) + self.assertFalse(err) + self.assertEqual(out, self.ssh_test_command_result) + + # Cancel pending reboot - we do want to proceed with the remaining tests + self.op_mode(['reboot', 'cancel']) + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_system_logs.py b/smoketest/scripts/cli/test_system_logs.py index 17cce5ca1..ccddcdec6 100755 --- a/smoketest/scripts/cli/test_system_logs.py +++ b/smoketest/scripts/cli/test_system_logs.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,18 +18,20 @@ import re import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.utils.file import read_file +from vyos.xml_ref import default_value # path to logrotate configs logrotate_atop_file = '/etc/logrotate.d/vyos-atop' logrotate_rsyslog_file = '/etc/logrotate.d/vyos-rsyslog' -# default values -default_atop_maxsize = '10M' -default_atop_rotate = '10' -default_rsyslog_size = '1M' -default_rsyslog_rotate = '10' base_path = ['system', 'logs'] +# default values +default_atop_maxsize = f"{default_value(base_path + ['logrotate', 'atop', 'max-size'])}M" +default_atop_rotate = default_value(base_path + ['logrotate', 'atop', 'rotate']) +default_rsyslog_size = f"{default_value(base_path + ['logrotate', 'messages', 'max-size'])}M" +default_rsyslog_rotate = default_value(base_path + ['logrotate', 'messages', 'rotate']) + def logrotate_config_parse(file_path): # read the file @@ -56,10 +58,11 @@ def logrotate_config_parse(file_path): class TestSystemLogs(VyOSUnitTestSHIM.TestCase): - def tearDown(self): self.cli_delete(base_path) self.cli_commit() + # always forward to base class + super().tearDown() def test_logs_defaults(self): # test with empty section for default values @@ -114,4 +117,4 @@ class TestSystemLogs(VyOSUnitTestSHIM.TestCase): if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_system_option.py b/smoketest/scripts/cli/test_system_option.py index f3112cf0b..e025c01ab 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 VyOS maintainers and contributors <maintainers@vyos.io> # # 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,18 +16,24 @@ 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'] - class TestSystemOption(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.cli_delete(base_path) self.cli_commit() + # always forward to base class + super().tearDown() def test_ctrl_alt_delete(self): self.cli_set(base_path + ['ctrl-alt-delete', 'reboot']) @@ -78,9 +84,9 @@ class TestSystemOption(VyOSUnitTestSHIM.TestCase): self.assertTrue(is_systemd_service_active(tuned_service)) - self.assertEqual(sysctl_read('net.ipv4.neigh.default.gc_thresh1'), gc_thresh1) - self.assertEqual(sysctl_read('net.ipv4.neigh.default.gc_thresh2'), gc_thresh2) - self.assertEqual(sysctl_read('net.ipv4.neigh.default.gc_thresh3'), gc_thresh3) + self.assertEqual(sysctl_read(['net', 'ipv4', 'neigh', 'default', 'gc_thresh1']), gc_thresh1) + self.assertEqual(sysctl_read(['net', 'ipv4', 'neigh', 'default', 'gc_thresh2']), gc_thresh2) + self.assertEqual(sysctl_read(['net', 'ipv4', 'neigh', 'default', 'gc_thresh3']), gc_thresh3) def test_ssh_client_options(self): loopback = 'lo' @@ -96,6 +102,46 @@ 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' + nohz_full = '2' + rcu_no_cbs = '1,2,4-5' + + self.cli_set(['system', 'option', 'kernel', 'cpu', 'disable-nmi-watchdog']) + 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', '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' nohz_full={nohz_full}', tmp) + self.assertIn(f' rcu_nocbs={rcu_no_cbs}', 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) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_system_resolvconf.py b/smoketest/scripts/cli/test_system_resolvconf.py index d8726a301..ad31e1f21 100755 --- a/smoketest/scripts/cli/test_system_resolvconf.py +++ b/smoketest/scripts/cli/test_system_resolvconf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -60,6 +60,8 @@ class TestSystemResolvConf(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path_domainname) self.cli_delete(base_path_domainsearch) self.cli_commit() + # always forward to base class + super().tearDown() def test_nameserver(self): # Check if server is added to resolv.conf @@ -109,4 +111,4 @@ class TestSystemResolvConf(VyOSUnitTestSHIM.TestCase): self.assertTrue(s not in domain_search) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_system_sflow.py b/smoketest/scripts/cli/test_system_sflow.py index 700253e2b..c6606e116 100755 --- a/smoketest/scripts/cli/test_system_sflow.py +++ b/smoketest/scripts/cli/test_system_sflow.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -42,13 +42,13 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase): def tearDown(self): # after service removal process must no longer run self.assertTrue(process_named_running(PROCESS_NAME)) - self.cli_delete(base_path) self.cli_delete(['vrf', 'name', vrf]) self.cli_commit() - # after service removal process must no longer run self.assertFalse(process_named_running(PROCESS_NAME)) + # always forward to base class + super().tearDown() def test_sflow(self): agent_address = '192.0.2.5' @@ -152,4 +152,4 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase): self.assertIn(PROCESS_NAME, tmp) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_system_syslog.py b/smoketest/scripts/cli/test_system_syslog.py index 6eae3f19d..2d1e0aef4 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-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 from base_vyostest_shim import VyOSUnitTestSHIM @@ -26,11 +27,44 @@ from vyos.xml_ref import default_value PROCESS_NAME = 'rsyslogd' RSYSLOG_CONF = '/run/rsyslog/rsyslog.conf' +CERT_DIR = '/etc/rsyslog.d/certs' base_path = ['system', 'syslog'] +base_logs_path = ['system', 'logs'] +pki_base = ['pki'] dummy_interface = 'dum372874' +ca_cert_name = "syslog_ca_certificate" +ca_cert = """ +MIIBrTCCAV+gAwIBAgIUdTEOleLyGTteZC+yEi252lRUq8EwBQYDK2VwMEsxCzAJ +BgNVBAYTAlVTMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEMMAoGA1UE +CgwDT3JnMQ8wDQYDVQQDDAZSb290Q0EwIBcNMjUwOTE1MTQxNDI4WhgPMjEyNTA4 +MjIxNDE0MjhaMEsxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UE +BwwEQ2l0eTEMMAoGA1UECgwDT3JnMQ8wDQYDVQQDDAZSb290Q0EwKjAFBgMrZXAD +IQCtTlgU+aqU/i6k6b318vebALk0zs9RvE96vw7taIt2iqNTMFEwHQYDVR0OBBYE +FHl8GywRMCWSotNGmyjuvRbPqCq8MB8GA1UdIwQYMBaAFHl8GywRMCWSotNGmyju +vRbPqCq8MA8GA1UdEwEB/wQFMAMBAf8wBQYDK2VwA0EAouZ4s+/ZeZxZxOZ7yFG0 +RQ9BfPWySrX4kgavyJJeg8LNCYUIRIP6iC41MTyHUVsWwar91xBT0DKBkpwrOQ0n +Dg== +""" + +client_cert_name = "syslog_client_certificate" +client_cert = """ +MIIBVjCCAQgCFArrkIM+zg8luHbXwsS8cUB5xrh/MAUGAytlcDBLMQswCQYDVQQG +EwJVUzEOMAwGA1UECAwFU3RhdGUxDTALBgNVBAcMBENpdHkxDDAKBgNVBAoMA09y +ZzEPMA0GA1UEAwwGUm9vdENBMB4XDTI1MDkxNTE0MTUwN1oXDTM1MDkxMzE0MTUw +N1owUDELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5 +MQwwCgYDVQQKDANPcmcxFDASBgNVBAMMC2V4YW1wbGUuY29tMCowBQYDK2VwAyEA +eZZRz7yVQ+exm6vyh/GdGZrTSEmtbvfafG0digqpfnUwBQYDK2VwA0EAU8/kw1i0 +s4j2fPQmU1q6Qql3xaxUlDyzhRPSIeH7ZhOlNg8R7gR1QnA7Rel6oU4EqJJHvz9l +83HQAy7ZcNIoBw== +""" + +client_cert_key = """ +MC4CAQAwBQYDK2VwBCIEIG59XPVZoMCxBVD/eJVqJSmV+Uc0bUHjHS4bkfkjM6Jj +""" + def get_config(string=''): """ Retrieve current "running configuration" from FRR @@ -50,35 +84,163 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): # out the current configuration :) cls.cli_delete(cls, base_path) cls.cli_delete(cls, ['vrf']) + cls.cli_delete(cls, pki_base) def tearDown(self): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) + # delete test certificates for syslog + self.cli_delete(pki_base) + # delete testing SYSLOG config self.cli_delete(base_path) + self.cli_delete(base_logs_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)) + # always forward to base class + super().tearDown() + + def _set_tls_certificates(self): + self.cli_set( + pki_base + ['ca', ca_cert_name, 'certificate', ca_cert.replace('\n', '')] + ) + self.cli_set( + pki_base + + [ + 'certificate', + client_cert_name, + 'certificate', + client_cert.replace('\n', ''), + ] + ) + self.cli_set( + pki_base + + [ + 'certificate', + client_cert_name, + 'private', + 'key', + client_cert_key.replace('\n', ''), + ] + ) + + def _set_facilities(self, base, facility_map): + for facility, facility_options in facility_map.items(): + level = facility_options['level'] + self.cli_set(base + ['facility', facility, 'level'], value=level) + + # Build prifilt selector strings the same way as rsyslog.conf.j2, e.g. + # "auth.info,*.notice;auth.none" when specific facilities coexist with "all". + def _prifilt_selectors(self, facility_map): + keys = sorted(facility_map) + specific = [] + for facility in keys: + if facility != 'all': + specific.append(facility) + + selectors = [] + for facility in keys: + opts = facility_map[facility] + level = opts['level'].replace('all', 'debug') + if facility == 'all': + sel = f'*.{level}' + for sf in specific: + sel += f';{sf}.none' + else: + sel = f'{facility}.{level}' + selectors.append(sel) + + prifilt = ','.join(selectors) + self._assert_prifilt_sane(prifilt, facility_map) + return prifilt + + def _assert_prifilt_sane(self, prifilt, facility_map): + has_all = 'all' in facility_map + specific_facilities = sorted(f for f in facility_map if f != 'all') + self.assertTrue(prifilt) + self.assertNotIn(' ', prifilt) + parts = prifilt.split(',') + self.assertTrue(all(parts)) + wildcard_parts = [p for p in parts if p.startswith('*.')] + if has_all: + self.assertEqual(len(wildcard_parts), 1) + wildcard = wildcard_parts[0] + if specific_facilities: + base, *exclusions = wildcard.split(';') + self.assertTrue(base.startswith('*.')) + expected = {f'{fac}.none' for fac in specific_facilities} + self.assertEqual(set(exclusions), expected) + else: + self.assertNotIn(';', wildcard) + else: + self.assertEqual(len(wildcard_parts), 0) + self.assertNotIn(';', prifilt) + + for part in parts: + if ';' in part: + base, *exclusions = part.split(';') + self.assertTrue(base.startswith('*.')) + self.assertNotIn('*.none', base) + for ex in exclusions: + self.assertTrue(ex.endswith('.none')) + self.assertFalse(ex.startswith('*.')) + self.assertNotIn(',', ex) + else: + if part.startswith('*.'): + self.assertTrue(has_all) + continue + self.assertEqual(part.count('.'), 1) + self.assertFalse(part.startswith('*.')) def test_console(self): - level = 'warning' - self.cli_set(base_path + ['console', 'facility', 'all', 'level'], value=level) + facility = { + 'all': {'level': 'warning'}, + } + self._set_facilities(base_path + ['console'], facility) 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) + expected_prifilt = self._prifilt_selectors(facility) + self.assertIn(f'if prifilt("{expected_prifilt}") then {{', rsyslog_conf) + self.assertIn('action(type="omfile" file="/dev/console")', rsyslog_conf) + + self.cli_delete(base_path + ['console']) + facility = { + 'auth': {'level': 'info'}, + 'kern': {'level': 'debug'}, + } + self._set_facilities(base_path + ['console'], facility) + self.cli_commit() + + rsyslog_conf = get_config() + expected_prifilt = self._prifilt_selectors(facility) + self.assertIn(f'if prifilt("{expected_prifilt}") then {{', rsyslog_conf) + self.assertIn('action(type="omfile" file="/dev/console")', rsyslog_conf) + + facility['all'] = {'level': 'notice'} + self._set_facilities(base_path + ['console'], facility) + self.cli_commit() + + rsyslog_conf = get_config() + expected_prifilt = self._prifilt_selectors(facility) + self.assertIn(f'if prifilt("{expected_prifilt}") then {{', rsyslog_conf) + self.assertIn('action(type="omfile" file="/dev/console")', rsyslog_conf) def test_basic(self): hostname = 'vyos123' domain_name = 'example.local' default_marker_interval = default_value(base_path + ['marker', 'interval']) + default_rsyslog_max_size = default_value( + base_logs_path + ['logrotate', 'messages', 'max-size'] + ) facility = { 'auth': {'level': 'info'}, @@ -90,9 +252,7 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): 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._set_facilities(base_path + ['local'], facility) self.cli_commit() @@ -106,21 +266,13 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): 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) + expected_prifilt = self._prifilt_selectors(facility) + self.assertIn(f'if prifilt("{expected_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) + size_limit = int(default_rsyslog_max_size) * 1024 * 1024 + self.assertIn(f' rotation.sizeLimit="{size_limit}"', config) self.assertIn( ' rotation.sizeLimitCommand="/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog"', config) self.cli_set(base_path + ['marker', 'disable']) @@ -166,10 +318,7 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): 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) + self._set_facilities(remote_base, remote_options['facility']) if 'format' in remote_options: for format in remote_options['format']: @@ -193,16 +342,9 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): config = read_file(RSYSLOG_CONF) for remote, remote_options in rhosts.items(): config = get_config(f'# Remote syslog to {remote}') - prifilt = [] + 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) + prifilt = self._prifilt_selectors(remote_options['facility']) if not prifilt: # Skip test - as we do not render anything if no facility is set continue @@ -233,6 +375,150 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): # cleanup dummy interface self.cli_delete(dummy_if_path) + def test_remote_tls(self): + self._set_tls_certificates() + + rhosts = { + '172.10.0.1': { + 'facility': {'all': {'level': 'debug'}}, + 'port': '6514', + 'protocol': 'tcp', + 'tls': {}, + }, + '172.10.0.2': { + 'facility': {'all': {'level': 'debug'}}, + 'port': '6514', + 'protocol': 'tcp', + 'tls': { + 'auth-mode': 'anon', + }, + }, + '172.10.0.3': { + 'facility': {'all': {'level': 'debug'}}, + 'port': '6514', + 'protocol': 'tcp', + 'tls': { + 'ca-certificate': ca_cert_name, + 'auth-mode': 'certvalid', + }, + }, + '172.10.0.4': { + 'facility': {'all': {'level': 'debug'}}, + 'port': '6514', + 'protocol': 'tcp', + 'tls': { + 'ca-certificate': ca_cert_name, + 'certificate': client_cert_name, + 'auth-mode': 'fingerprint', + 'permitted-peer': [ + 'SHA1:E1:DB:C4:FF:83:54:85:40:2D:56:E7:1A:C3:FF:70:22:0F:21:74:ED', + ' SHA1:FF:70:22:0F:21:74:ED:54:85:40:2D:56:E7:1A:C3:E1:DB:C4:FF:83 ', + ], + }, + }, + '172.10.0.5': { + 'facility': {'all': {'level': 'debug'}}, + 'port': '6514', + 'protocol': 'tcp', + 'tls': { + 'ca-certificate': ca_cert_name, + 'certificate': client_cert_name, + 'auth-mode': 'name', + 'permitted-peer': [ + 'logs.example.com', + ' ', + ], + }, + }, + } + + 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 'protocol' in remote_options: + protocol = remote_options['protocol'] + self.cli_set(remote_base + ['protocol'], value=protocol) + + tls = remote_options['tls'] + if tls: + for key, value in tls.items(): + if type(value) is list: + values = value + for value in values: + self.cli_set(remote_base + ['tls', key], value=value) + else: + self.cli_set(remote_base + ['tls', key], value=value) + else: + self.cli_set(remote_base + ['tls']) + + self.cli_commit() + + read_file(RSYSLOG_CONF) + for remote, remote_options in rhosts.items(): + with self.subTest(remote=remote): + config = get_config(f'# Remote syslog to {remote}') + + if 'port' in remote_options: + port = remote_options['port'] + self.assertIn(f'port="{port}"', config) + + self.assertIn('protocol="tcp"', config) + self.assertIn('StreamDriver="ossl"', config) + self.assertIn('StreamDriverMode="1"', config) + + tls = remote_options['tls'] + if 'ca-certificate' in tls: + self.assertIn( + f'StreamDriver.CAFile="{CERT_DIR}/{ca_cert_name}.pem"', config + ) + + if 'certificate' in tls: + self.assertIn( + f'StreamDriver.CertFile="{CERT_DIR}/{client_cert_name}.pem"', + config, + ) + self.assertIn( + f'StreamDriver.KeyFile="{CERT_DIR}/{client_cert_name}.key"', + config, + ) + + if 'auth-mode' in tls: + value = tls['auth-mode'] + auth_mode = value if value == 'anon' else f'x509/{value}' + self.assertIn(f'StreamDriverAuthMode="{auth_mode}"', config) + + if 'permitted-peer' in tls: + values = tls['permitted-peer'] + value = ','.join([v.strip() for v in values if v.strip()]) + self.assertIn(f'StreamDriverPermittedPeers="{value}"', config) + + if not tls: + self.assertIn('StreamDriverAuthMode="anon"', config) + + def test_remote_tls_protocol_udp(self): + remote_base = base_path + ['remote', '172.11.0.1'] + self.cli_set(remote_base + ['port'], value='6514') + self.cli_set(remote_base + ['facility', 'all', 'level'], value='debug') + self.cli_set(remote_base + ['protocol'], value='udp') + self.cli_set(remote_base + ['tls']) + + err_msg = "TLS is enabled for remote \"172.11.0.1\", but protocol is set to UDP" + with self.assertRaisesRegex(ConfigSessionError, err_msg): + self.cli_commit() + + self.cli_set(base_path + ['remote', '172.11.0.1', 'protocol'], value='tcp') + self.cli_commit() + def test_vrf_source_address(self): rhosts = { '169.254.0.10': { }, @@ -299,4 +585,4 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): self.cli_delete(['interfaces', 'dummy', f'dum{idx}']) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_system_watchdog.py b/smoketest/scripts/cli/test_system_watchdog.py new file mode 100644 index 000000000..a263540f7 --- /dev/null +++ b/smoketest/scripts/cli/test_system_watchdog.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> +# +# 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 cmd + +base_path = ['system', 'watchdog'] + + +class TestSystemWatchdog(VyOSUnitTestSHIM.TestCase): + def tearDown(self): + self.cli_delete(base_path) + self.cli_commit() + super().tearDown() + + def test_enable_watchdog_softdog(self): + """Configure watchdog (presence enables) with softdog and check state""" + # Presence of 'system watchdog' enables watchdog; set module to softdog + self.cli_set(base_path) + self.cli_set(base_path + ['module', 'softdog']) + self.cli_commit() + # Check if softdog module is loaded + lsmod = cmd('lsmod') + self.assertIn('softdog', lsmod) + # Check /dev/watchdog0 exists + self.assertTrue( + os.path.exists('/dev/watchdog0'), '/dev/watchdog0 does not exist' + ) + # Check systemd config file exists + config_path = '/run/systemd/system.conf.d/watchdog.conf' + self.assertTrue( + os.path.exists(config_path), f"Systemd config file not found: {config_path}" + ) + + def test_invalid_module_rejected(self): + """Verify that a non-existent watchdog module causes commit failure""" + # Choose a module name unlikely to exist; include a prefix to avoid collision with real names + bogus_module = 'zzzx_watchdog_unit_test_fake' + self.cli_set(base_path) + + # Module validation is preferred at set-time. Depending on the test harness, + # this may raise on cli_set() or on cli_commit(). Accept either. + try: + self.cli_set(base_path + ['module', bogus_module]) + except Exception as e: + self.assertRegex( + str(e), r"Module must be an available watchdog kernel driver module" + ) + return + + # If set-time validation did not trigger, commit-time validation must. + with self.assertRaisesRegex( + Exception, + r"Watchdog( driver)? module '.*' was not found or cannot be loaded", + ): + self.cli_commit() + + def test_timeout_upper_limit(self): + """Verify watchdog timeout upper bound (65535) is enforced""" + self.cli_set(base_path) + self.cli_set(base_path + ['module', 'softdog']) + + # 65535 must be accepted + self.cli_set(base_path + ['timeout', '65535']) + self.cli_commit() + + # 65536 must be rejected (ideally at set-time by XML validator) + try: + self.cli_set(base_path + ['timeout', '65536']) + except Exception as e: + # Error message depends on validator/harness formatting + self.assertRegex(str(e), r"65535|Timeout must be between") + return + + with self.assertRaisesRegex(Exception, r"65535|Timeout must be between"): + self.cli_commit() + + def test_shutdown_and_reboot_timeout_written(self): + """Verify shutdown-timeout and reboot-timeout are applied to systemd config""" + self.cli_set(base_path) + self.cli_set(base_path + ['module', 'softdog']) + + # Lowest valid values + self.cli_set(base_path + ['shutdown-timeout', '60']) + self.cli_set(base_path + ['reboot-timeout', '60']) + self.cli_commit() + + config_path = '/run/systemd/system.conf.d/watchdog.conf' + with open(config_path, 'r') as f: + conf = f.read() + self.assertIn('ShutdownWatchdogSec=60', conf) + self.assertIn('RebootWatchdogSec=60', conf) + + # Highest valid values + self.cli_set(base_path + ['shutdown-timeout', '65535']) + self.cli_set(base_path + ['reboot-timeout', '65535']) + self.cli_commit() + + with open(config_path, 'r') as f: + conf = f.read() + self.assertIn('ShutdownWatchdogSec=65535', conf) + self.assertIn('RebootWatchdogSec=65535', conf) + + def test_shutdown_and_reboot_timeout_lower_bound(self): + """Verify shutdown-timeout/reboot-timeout enforce lower bound (60)""" + self.cli_set(base_path) + self.cli_set(base_path + ['module', 'softdog']) + self.cli_commit() + + for key in ['shutdown-timeout', 'reboot-timeout']: + try: + self.cli_set(base_path + [key, '59']) + except Exception as e: + self.assertRegex( + str(e), r"60|timeout must be between|Timeout must be between" + ) + continue + + with self.assertRaisesRegex( + Exception, r"60|timeout must be between|Timeout must be between" + ): + self.cli_commit() + + +if __name__ == '__main__': + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_techsupport_archive.py b/smoketest/scripts/cli/test_techsupport_archive.py new file mode 100644 index 000000000..f345d20eb --- /dev/null +++ b/smoketest/scripts/cli/test_techsupport_archive.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +# +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> +# +# 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 io +import re +import contextlib +import pathlib +import tarfile +import unittest + +from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.utils.process import call +from vyos.utils.file import get_name_from_path + +base_path = ['generate tech-support archive'] +testdir = pathlib.Path('/tmp/_test_techsupport_archive') + + +def tar_gz_paths(archive_path: pathlib.Path, max_depth: int = 5) -> set: + """ + Return all member paths inside `archive_path`. + + Nested archives are expanded recursively and represented as: + "inner.tar.gz::path/inside/inner" + """ + + def is_nested(name: str) -> bool: + nested_suffixes = ('.tar.gz', '.tgz', '.tar') + return name.lower().endswith(nested_suffixes) + + out = set() + + def walk_bytes(data: bytes, prefix: str, depth: int): + if depth > max_depth: + return + + bio = io.BytesIO(data) + with tarfile.open(fileobj=bio, mode='r:*') as tf: + for member in tf.getmembers(): + out.add(f'{prefix}::{member.name}' if prefix else member.name) + + if member.isreg() and is_nested(member.name) and depth < max_depth: + file = tf.extractfile(member) + if file is None: + continue + + sub_prefix = f'{prefix}::{member.name}' if prefix else member.name + walk_bytes(file.read(), sub_prefix, depth + 1) + + walk_bytes(archive_path.read_bytes(), prefix='', depth=0) + + return out + + +@contextlib.contextmanager +def stub_files(file_paths: list[str]): + """ + Context manager for tests. + + Creates a temporary set of files and removes it after use. + Example: + with stub_files(['a.txt', 'dir/b.txt']): + pass + """ + + paths = [pathlib.Path(file_path) for file_path in file_paths] + + try: + for path in paths: + if path.parent.exists(): + path.touch() + + yield + + finally: + for path in paths: + path.unlink(missing_ok=True) + + +class TestTechSupportArchive(VyOSUnitTestSHIM.TestCase): + def tearDown(self): + # always forward to base class + super().tearDown() + + if testdir.exists(): + call(f'sudo rm -rf {testdir}') + + def _check_path(self, path: str | pathlib.Path, state: bool, err_message: str): + if isinstance(path, str): + path = pathlib.Path(path) + + if state: + self.assertTrue(path.exists(), err_message) + else: + self.assertFalse(path.exists(), err_message) + + def _extract_archive_path(self, output: str) -> pathlib.Path: + match = re.search(r'located in ([^\s]+\.tar\.gz)', output) + path = match.group(1) if match else None + + err_message = ( + 'It is not possible to extract the path to the resulting ' + 'archive from stdout:' + f'\n```\n{output}\n```' + ) + assert path is not None, err_message + + return pathlib.Path(path) + + def _extract_archive_inner_path_prefix(self, archive_path: pathlib.Path): + # Convert + # `bdbdd9a4807f_tech-support-archive_2026-02-02T15-33-26.tar.gz` + # to + # `bdbdd9a4807f_tech-support-archive_2026-02-02T15-33-26` + + return get_name_from_path(archive_path) + + def assertPathExists(self, path: str): + err_message = f'Path `{path}` does not exist after generating a archive' + self._check_path(path, True, err_message) + + def assertNotPathExists(self, path: str): + err_message = f'Path `{path}` still exists after generating a archive' + self._check_path(path, False, err_message) + + def assertExpectedArcPaths( + self, archive_path: pathlib.Path, expected_paths: list[str] + ): + actual = tar_gz_paths(archive_path) + expected = frozenset(expected_paths) + + missing = sorted(expected - actual) + + if missing: + lines = [f'Archive paths mismatch: {archive_path}', 'Missing:'] + lines += [f' - {p}' for p in missing] + raise AssertionError('\n'.join(lines)) + + def assertNotExpectedArcPaths( + self, archive_path: pathlib.Path, unexpected_paths: list[str] + ): + actual = tar_gz_paths(archive_path) + unexpected = frozenset(unexpected_paths) + + extra = sorted(actual & unexpected) + + if extra: + lines = [f'Archive paths mismatch: {archive_path}', 'Unexpected:'] + lines += [f' - {p}' for p in extra] + raise AssertionError('\n'.join(lines)) + + def test_general_archive_structure(self): + output = self.op_mode(base_path) + + archive_path = self._extract_archive_path(output) + self.assertPathExists(archive_path) + self.assertEqual(str(archive_path.parent), '/tmp') + + prefix = self._extract_archive_inner_path_prefix(archive_path) + self.assertExpectedArcPaths( + archive_path, + [ + f'{prefix}/show_tech-support_report/vyos-main-info', + f'{prefix}/config.tar.gz::opt/vyatta/etc/config/config.boot', + f'{prefix}/core-dump.tar.gz::var/core', + f'{prefix}/home.tar.gz::home/vyos/.profile', + f'{prefix}/root.tar.gz::root/.profile', + f'{prefix}/run.tar.gz::run', + f'{prefix}/tmp.tar.gz::tmp/vyos-config-status', + f'{prefix}/topology-logical.png', + f'{prefix}/topology.png', + f'{prefix}/var-log.tar.gz::var/log/journal', + ], + ) + + if archive_path.exists(): + call(f'sudo rm -f {archive_path}') + + def test_excluded_archive_files(self): + fake_files = [ + '/opt/vyatta/etc/config/_test_fake_vyos.iso', + '/home/vyos/_test_fake_vyos.iso', + '/tmp/_test_fake_vyos.iso', + '/opt/vyatta/etc/config/_test_fake_archive.tar.gz', + '/home/vyos/_test_fake_archive.tar.gz', + '/tmp/_test_fake_archive.tar.gz', + '/home/vyos/drops-debug_9999-12-31T23-59-59', + '/home/vyos/ffffffffffff_tech-support-archive_9999-12-31T23-59-59', + '/tmp/drops-debug_9999-12-31T23-59-59', + '/tmp/ffffffffffff_tech-support-archive_9999-12-31T23-59-59', + ] + with stub_files(fake_files): + output = self.op_mode(base_path) + + archive_path = self._extract_archive_path(output) + self.assertPathExists(archive_path) + + prefix = self._extract_archive_inner_path_prefix(archive_path) + self.assertNotExpectedArcPaths( + archive_path, + [ + f'{prefix}/config.tar.gz::opt/vyatta/etc/config/_test_fake_vyos.iso', + f'{prefix}/home.tar.gz::home/vyos/_test_fake_vyos.iso', + f'{prefix}/tmp.tar.gz::tmp/_test_fake_vyos.iso', + f'{prefix}/var.tar.gz::var/log/messages', + f'{prefix}/var.tar.gz::var/log/messages.1', + f'{prefix}/config.tar.gz::opt/vyatta/etc/config/_test_fake_archive.tar.gz', + f'{prefix}/home.tar.gz::home/vyos/_test_fake_archive.tar.gz', + f'{prefix}/tmp.tar.gz::tmp/_test_fake_archive.tar.gz', + f'{prefix}/home.tar.gz::home/vyos/drops-debug_9999-12-31T23-59-59', + f'{prefix}/home.tar.gz::home/vyos/ffffffffffff_tech-support-archive_9999-12-31T23-59-59', + f'{prefix}/tmp.tar.gz::tmp/drops-debug_9999-12-31T23-59-59', + f'{prefix}/tmp.tar.gz::tmp/ffffffffffff_tech-support-archive_9999-12-31T23-59-59', + ], + ) + + if archive_path.exists(): + call(f'sudo rm -f {archive_path}') + + def test_custom_archive_path(self): + output = self.op_mode(base_path + [str(testdir / 'foo')]) + + archive_path = self._extract_archive_path(output) + self.assertPathExists(archive_path) + self.assertEqual(archive_path.parent, testdir) + self.assertEqual(archive_path.name, 'foo.tar.gz') + + def test_custom_directory_archive_path(self): + output = self.op_mode(base_path + [str(testdir) + '/bar/']) + + archive_path = self._extract_archive_path(output) + self.assertPathExists(archive_path) + self.assertEqual(archive_path.parent.name, 'bar') + self.assertEqual(archive_path.suffix, '.gz') + + +if __name__ == '__main__': + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_techsupport_report.py b/smoketest/scripts/cli/test_techsupport_report.py new file mode 100644 index 000000000..af81be182 --- /dev/null +++ b/smoketest/scripts/cli/test_techsupport_report.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> +# +# 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 pathlib +import shutil +import unittest + +from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.defaults import directories +from vyos.utils.process import cmd + +base_path = ['show tech-support report'] +script_path = directories['op_mode'] + '/show_techsupport_report.py' +testdir = pathlib.Path('/tmp/_test_techsupport_report') + +all_blocks = ( + 'vyos-main-info', + 'routing-info', + 'frr-info', + 'proc-and-sysctl-info', + 'net-and-processes-info', + 'ethtool-info', + 'lspci-and-numa-info', + 'nftables-info', + 'dpkg-and-modules-info', + 'system-resources-info', + 'ipsec-debug-info', + 'vpp-info', +) + + +class TestTechSupportReport(VyOSUnitTestSHIM.TestCase): + def _gen_section_header(self, name: str) -> str: + length = len(name) + return '=' * length + '\n' + name + '\n' + '=' * length + '\n' + + def assertSectionIn(self, name: str, report: str): + header = self._gen_section_header(name) + self.assertIn(header, report, f'Report does not contain section `{name}`') + + def assertSectionNotIn(self, name: str, report: str): + header = self._gen_section_header(name) + self.assertNotIn( + header, report, f'Report contains unnecessary section `{name}`' + ) + + def test_full_report(self): + report = self.op_mode(base_path) + for block in all_blocks: + self.assertSectionIn(block, report) + + def test_filtered_report(self): + blocks = ( + 'vyos-main-info', + 'proc-and-sysctl-info', + ) + + report = cmd([script_path, '--reports'] + list(blocks)) + + for block in blocks: + self.assertSectionIn(block, report) + + missing_blocks = frozenset(all_blocks) - frozenset(blocks) + for block in missing_blocks: + self.assertSectionNotIn(block, report) + + def test_directory_output(self): + cmd([script_path, '--outdir', str(testdir)]) + + for block in all_blocks: + file_path = testdir / block + err_message = f'File `{file_path}` does not exist after generating a report' + + self.assertTrue(file_path.exists(), err_message) + self.assertSectionIn(block, file_path.read_text()) + + shutil.rmtree(testdir, ignore_errors=True) + + +if __name__ == '__main__': + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py index 91a76e6f6..68aef845e 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-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,6 +16,7 @@ import os import unittest +import re from base_vyostest_shim import VyOSUnitTestSHIM @@ -24,6 +25,8 @@ 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 +from vyos.xml_ref import default_value + ethernet_path = ['interfaces', 'ethernet'] tunnel_path = ['interfaces', 'tunnel'] @@ -44,6 +47,7 @@ vif = '100' esp_group = 'MyESPGroup' ike_group = 'MyIKEGroup' secret = 'MYSECRETKEY' +ppk_secret_hex = '55c2ebca1bada7ac0e4e1390a8dbb563cefea0c7bd59f4f2c86a627f5927fb90' PROCESS_NAME = 'charon-systemd' regex_uuid4 = '[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}' @@ -107,6 +111,11 @@ swanctl_dir = '/etc/swanctl' CERT_PATH = f'{swanctl_dir}/x509/' CA_PATH = f'{swanctl_dir}/x509ca/' +def get_config_value(file, key): + tmp = read_file(file) + tmp = re.findall(f'\n?{key}\s+(.*)', tmp) + return tmp + class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): skip_process_check = False @@ -126,6 +135,9 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): cls.cli_delete(cls, base_path + ['interface', f'{interface}.{vif}']) def setUp(self): + # always forward to base class + super().setUp() + # Set IKE/ESP Groups self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes128']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'hash', 'sha1']) @@ -147,6 +159,8 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): # Check for no longer running process self.assertFalse(process_named_running(PROCESS_NAME)) + # always forward to base class + super().tearDown() def setupPKI(self): self.cli_set(['pki', 'ca', ca_name, 'certificate', ca_pem.replace('\n','')]) @@ -225,6 +239,9 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.cli_set(peer_base_path + ['tunnel', '2', 'remote', 'prefix', '10.2.0.0/16']) self.cli_set(peer_base_path + ['tunnel', '2', 'priority', priority]) + # Passing the 'unique = never' for StrongSwan's `connections.<conn>.unique` parameter + self.cli_set(base_path + ['disable-uniqreqids']) + self.cli_commit() # Verify strongSwan configuration @@ -251,14 +268,15 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): f'priority = {priority}', f'mode = tunnel', f'replay_window = 32', + 'unique = never', ] for line in swanctl_conf_lines: self.assertIn(line, swanctl_conf) # if dpd is not specified it should not be enabled (see T6599) swanctl_unexpected_lines = [ - f'dpd_timeout' - f'dpd_delay' + 'dpd_timeout', + 'dpd_delay', ] for unexpected_line in swanctl_unexpected_lines: @@ -274,6 +292,218 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): for line in swanctl_secrets_lines: self.assertRegex(swanctl_conf, fr'{line}') + def test_site_to_site_ts_protocol_all(self): + """ + Test acceptance of 'all' protocol in site-to-site traffic selector. + + Verifies that specifying only the subnet (e.g., 'x.x.x.0/24') is accepted + for "all" protocols in IPsec site-to-site configuration, while explicit + '[all/]' protocol syntax is rejected with strongSwan 5.9.x. + + More details: https://vyos.dev/T7581 + """ + + self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2']) + + local_address = '192.0.2.12' + + # vpn ipsec auth psk <tag> id <x.x.x.x> + auth_psk_path = base_path + ['authentication', 'psk', connection_name] + self.cli_set(auth_psk_path + ['id', local_id]) + self.cli_set(auth_psk_path + ['id', remote_id]) + self.cli_set(auth_psk_path + ['id', local_address]) + self.cli_set(auth_psk_path + ['id', peer_ip]) + self.cli_set(auth_psk_path + ['secret', secret]) + + # Site to site + peer_base_path = base_path + ['site-to-site', 'peer', connection_name] + tunnel_1_base_path = peer_base_path + ['tunnel', '1'] + tunnel_2_base_path = peer_base_path + ['tunnel', '2'] + + self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret']) + 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(tunnel_1_base_path + ['protocol', 'all']) + self.cli_set(tunnel_1_base_path + ['local', 'prefix', '172.16.10.0/24']) + self.cli_set(tunnel_1_base_path + ['local', 'port', '443']) + self.cli_set(tunnel_1_base_path + ['remote', 'prefix', '172.17.11.0/24']) + self.cli_set(tunnel_1_base_path + ['remote', 'port', '443']) + + self.cli_set(tunnel_2_base_path + ['protocol', 'all']) + self.cli_set(tunnel_2_base_path + ['local', 'prefix', '10.1.0.0/16']) + self.cli_set(tunnel_2_base_path + ['remote', 'prefix', '10.2.0.0/16']) + + self.cli_commit() + + # Verify strongSwan configuration + swanctl_conf = read_file(swanctl_file) + swanctl_conf_lines = [ + 'version = 2', + 'auth = psk', + f'local_addrs = {local_address} # dhcp:no', + f'remote_addrs = {peer_ip}', + 'mode = tunnel', + f'{connection_name}-tunnel-1', + 'local_ts = 172.16.10.0/24[/443]', + 'remote_ts = 172.17.11.0/24[/443]', + 'mode = tunnel', + f'{connection_name}-tunnel-2', + 'local_ts = 10.1.0.0/16', + 'remote_ts = 10.2.0.0/16', + 'mode = tunnel', + ] + for line in swanctl_conf_lines: + self.assertIn(line, swanctl_conf) + + def test_site_to_site_with_default_ts(self): + """Test 'site to site' with default value of local and remote Traffic Selection""" + + self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2']) + + local_address = '192.0.2.11' + life_bytes = '100000' + life_packets = '2000000' + + # 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', 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] + ) + + # Site to site + peer_base_path = base_path + ['site-to-site', 'peer', connection_name] + + self.cli_set(base_path + ['esp-group', esp_group, 'life-bytes', life_bytes]) + self.cli_set(base_path + ['esp-group', esp_group, 'life-packets', life_packets]) + + self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret']) + 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 + ['tunnel', '1', 'protocol', 'gre']) + + self.cli_commit() + + # Verify strongSwan configuration + swanctl_conf = read_file(swanctl_file) + swanctl_conf_lines = [ + f'version = 2', + f'auth = psk', + f'life_bytes = {life_bytes}', + f'life_packets = {life_packets}', + f'rekey_time = 28800s', # default value + f'proposals = aes128-sha1-modp1024', + f'esp_proposals = aes128-sha1-modp1024', + f'life_time = 3600s', # default value + f'local_addrs = {local_address} # dhcp:no', + f'remote_addrs = {peer_ip}', + f'mode = tunnel', + f'{connection_name}-tunnel-1', + f'local_ts = dynamic[gre/]', # default value + f'remote_ts = dynamic[gre/]', # default value + f'mode = tunnel', + ] + for line in swanctl_conf_lines: + self.assertIn(line, swanctl_conf) + + def test_site_to_site_gre_over_ipsec(self): + """Test GRE over IPsec site‑to‑site configuration with transport mode ESP""" + + tunnel_id = '100' + local_address = '172.168.99.2' + + # Interfaces + base_tun_path = tunnel_path + [f'tun{tunnel_id}'] + self.cli_set(base_tun_path + ['address', '10.12.0.1/30']) + self.cli_set(base_tun_path + ['encapsulation', 'gre']) + self.cli_set(base_tun_path + ['remote', peer_ip]) + self.cli_set(base_tun_path + ['source-address', local_address]) + self.cli_set(ethernet_path + [interface, 'vif', vif, 'address', 'dhcp']) + + # Authentication (PSK) + base_psk_path = base_path + ['authentication', 'psk'] + self.cli_set(base_psk_path + [peer_name, 'id', local_address]) + self.cli_set(base_psk_path + [peer_name, 'id', peer_ip]) + self.cli_set(base_psk_path + [peer_name, 'secret', secret]) + + # ESP group + base_esp_path = base_path + ['esp-group', esp_group] + self.cli_set(base_esp_path + ['lifetime', '3600']) + self.cli_set(base_esp_path + ['mode', 'transport']) + self.cli_set(base_esp_path + ['pfs', 'dh-group14']) + self.cli_set(base_esp_path + ['proposal', '10', 'encryption', 'aes256']) + self.cli_set(base_esp_path + ['proposal', '10', 'hash', 'sha1']) + + # IKE group + base_ike_path = base_path + ['ike-group', ike_group] + self.cli_set(base_ike_path + ['close-action', 'none']) + self.cli_set(base_ike_path + ['dead-peer-detection', 'action', 'restart']) + self.cli_set(base_ike_path + ['dead-peer-detection', 'interval', '10']) + self.cli_set(base_ike_path + ['key-exchange', 'ikev2']) + self.cli_set(base_ike_path + ['lifetime', '28800']) + self.cli_set(base_ike_path + ['proposal', '10', 'dh-group', '5']) + self.cli_set(base_ike_path + ['proposal', '10', 'encryption', 'aes256']) + self.cli_set(base_ike_path + ['proposal', '10', 'hash', 'sha1']) + + # IPsec interface binding + self.cli_set(base_path + ['interface', interface]) + + # Site‑to‑site peer + peer_path = base_path + ['site-to-site', 'peer', peer_name] + self.cli_set(peer_path + ['authentication', 'mode', 'pre-shared-secret']) + self.cli_set(peer_path + ['authentication', 'local-id', local_address]) + self.cli_set(peer_path + ['authentication', 'remote-id', peer_ip]) + self.cli_set(peer_path + ['connection-type', 'initiate']) + self.cli_set(peer_path + ['default-esp-group', esp_group]) + self.cli_set(peer_path + ['ike-group', ike_group]) + self.cli_set(peer_path + ['local-address', local_address]) + self.cli_set(peer_path + ['remote-address', peer_ip]) + self.cli_set(peer_path + ['tunnel', tunnel_id, 'protocol', 'gre']) + + # Commit and verify + self.cli_commit() + + # Verify strongSwan configuration + swanctl_conf = read_file(swanctl_file) + swanctl_conf_lines = [ + 'version = 2', + 'auth = psk', + 'proposals = aes128-sha1-modp1024,aes256-sha1-modp1536', + 'esp_proposals = aes128-sha1-modp2048,aes256-sha1-modp2048', + 'life_time = 3600s', + 'mode = transport', # ensure transport mode is used + f'{peer_name}-tunnel-{tunnel_id}', + f'local_ts = {local_address}[gre/]', # GRE tunnel source/target + f'remote_ts = {peer_ip}[gre/]', + f'local_addrs = {local_address} # dhcp:no', + f'remote_addrs = {peer_ip}', + ] + for line in swanctl_conf_lines: + with self.subTest(line=line): + self.assertIn(line, swanctl_conf) + + # Verify validation of local/remote prefix + base_tun_path = peer_path + ['tunnel', tunnel_id] + self.cli_set(base_tun_path + ['local', 'prefix', '10.1.2.0/24']) + self.cli_set(base_tun_path + ['remote', 'prefix', '10.4.5.0/24']) + + err_msg = 'Local/remote prefix cannot be used with ESP transport mode on tunnel' + with self.assertRaisesRegex(ConfigSessionError, err_msg): + self.cli_commit() def test_site_to_site_vti(self): local_address = '192.0.2.10' @@ -352,6 +582,227 @@ 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_site_to_site_nist_800_77_cnsa_1_with_ppk(self): + # Setup IKE group + self.cli_set(base_path + ['ike-group', 'cnsa1-ike', 'key-exchange', 'ikev2']) + self.cli_set(base_path + ['ike-group', 'cnsa1-ike', 'lifetime', '86400']) + self.cli_set( + base_path + ['ike-group', 'cnsa1-ike', 'proposal', '10', 'dh-group', '20'] + ) + self.cli_set( + base_path + + ['ike-group', 'cnsa1-ike', 'proposal', '10', 'encryption', 'aes256gcm128'] + ) + self.cli_set( + base_path + ['ike-group', 'cnsa1-ike', 'proposal', '10', 'hash', 'sha384'] + ) + self.cli_set( + base_path + ['ike-group', 'cnsa1-ike', 'proposal', '10', 'prf', 'prfsha384'] + ) + + # Setup ESP group + self.cli_set(base_path + ['esp-group', 'cnsa1-esp', 'lifetime', '28800']) + self.cli_set(base_path + ['esp-group', 'cnsa1-esp', 'mode', 'tunnel']) + self.cli_set(base_path + ['esp-group', 'cnsa1-esp', 'pfs', 'dh-group20']) + self.cli_set( + base_path + + ['esp-group', 'cnsa1-esp', 'proposal', '10', 'encryption', 'aes256gcm128'] + ) + self.cli_set( + base_path + ['esp-group', 'cnsa1-esp', 'proposal', '10', 'hash', 'sha384'] + ) + + local_address = '192.0.2.10' + + # 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', 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] + ) + + # vpn ipsec auth ppk <tag> id <name> + self.cli_set( + base_path + ['authentication', 'ppk', connection_name, 'id', 'ppk-test'] + ) + self.cli_set( + base_path + + ['authentication', 'ppk', connection_name, 'secret', ppk_secret_hex] + ) + self.cli_set( + base_path + ['authentication', 'ppk', connection_name, 'secret-type', 'hex'] + ) + + # 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']) + + # Require use of valid PPK + self.cli_set(peer_base_path + ['authentication', 'ppk', 'id', 'ppk-test']) + self.cli_set(peer_base_path + ['authentication', 'ppk', 'required']) + + # Set childless IKE_INIT to prefer + self.cli_set(peer_base_path + ['childless', 'prefer']) + + self.cli_set(peer_base_path + ['default-esp-group', 'cnsa1-esp']) + self.cli_set(peer_base_path + ['ike-group', 'cnsa1-ike']) + 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 + ['tunnel', '1', 'local', 'prefix', '172.16.10.0/24'] + ) + self.cli_set( + peer_base_path + ['tunnel', '1', 'remote', 'prefix', '172.17.10.0/24'] + ) + + self.cli_commit() + + # Verify strongSwan configuration + swanctl_conf = read_file(swanctl_file) + swanctl_conf_lines = [ + f'ppk_id = ppk-test', + f'ppk_required = yes', + f'childless = prefer', + f'version = 2', + f'auth = psk', + f'rekey_time = 86400s', + f'proposals = aes256gcm128-sha384-prfsha384-ecp384', + f'esp_proposals = aes256gcm128-sha384-ecp384', + f'life_time = 28800s', # default value + f'local_addrs = {local_address} # dhcp:no', + f'remote_addrs = {peer_ip}', + f'mode = tunnel', + f'{connection_name}-tunnel-1', + f'local_ts = 172.16.10.0/24', + f'remote_ts = 172.17.10.0/24', + f'mode = tunnel', + f'replay_window = 32', + ] + for line in swanctl_conf_lines: + self.assertIn(line, swanctl_conf) + + # if dpd is not specified it should not be enabled (see T6599) + swanctl_unexpected_lines = [ + 'dpd_timeout', + 'dpd_delay', + ] + + for unexpected_line in swanctl_unexpected_lines: + self.assertNotIn(unexpected_line, swanctl_conf) + + swanctl_secrets_lines = [ + f'id-{regex_uuid4} = "{local_id}"', + f'id-{regex_uuid4} = "{remote_id}"', + f'id-{regex_uuid4} = "{local_address}"', + f'id-{regex_uuid4} = "{peer_ip}"', + f'secret = "{secret}"', + f'ppk-{connection_name}', + f'id-{regex_uuid4} = "ppk-test"', + f'secret = 0x{ppk_secret_hex}', + ] + for line in swanctl_secrets_lines: + self.assertRegex(swanctl_conf, fr'{line}') + + def test_dmvpn(self): ike_lifetime = '3600' esp_lifetime = '1800' @@ -411,6 +862,9 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['profile', 'NHRPVPN', 'esp-group', esp_group]) self.cli_set(base_path + ['profile', 'NHRPVPN', 'ike-group', ike_group]) + # Passing the 'unique = never' for StrongSwan's `connections.<conn>.unique` parameter + self.cli_set(base_path + ['disable-uniqreqids']) + self.cli_commit() swanctl_conf = read_file(swanctl_file) @@ -423,7 +877,8 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): f'local_ts = dynamic[gre]', f'remote_ts = dynamic[gre]', f'mode = transport', - f'secret = {nhrp_secret}' + f'secret = {nhrp_secret}', + 'unique = never', ] for line in swanctl_lines: self.assertIn(line, swanctl_conf) @@ -506,6 +961,77 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): # Disable PKI self.tearDownPKI() + def test_site_to_site_ikev2_reauth(self): + # T7555: Verify ikev2-reauth is correctly written to swanctl.conf + # and that invalid combinations are rejected by validation + + local_address = '192.0.2.10' + ike_lifetime = '1800' + + # Base PSK auth used across all sub-tests + psk_base_path = base_path + ['authentication', 'psk', connection_name] + self.cli_set(psk_base_path + ['id', local_id]) + self.cli_set(psk_base_path + ['id', remote_id]) + self.cli_set(psk_base_path + ['id', local_address]) + self.cli_set(psk_base_path + ['id', peer_ip]) + self.cli_set(psk_base_path + ['secret', secret]) + + 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 + ['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 + ['tunnel', '1', 'local', 'prefix', '10.0.0.0/24'], + ) + self.cli_set( + peer_base_path + ['tunnel', '1', 'remote', 'prefix', '10.1.0.0/24'], + ) + + # ikev2-reauth on an IKEv1-only ike-group must be rejected + self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev1']) + self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', ike_lifetime]) + self.cli_set(peer_base_path + ['ike-group', ike_group]) + self.cli_set(peer_base_path + ['ikev2-reauth', 'yes']) + + err_msg = 'ikev2-reauth requires key-exchange ikev2 in IKE group' + with self.assertRaisesRegex(ConfigSessionError, err_msg): + self.cli_commit() + + # Switch to IKEv2, enable reauth on the ike-group (valueless flag) + self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2']) + self.cli_set(base_path + ['ike-group', ike_group, 'ikev2-reauth']) + self.cli_set(peer_base_path + ['ikev2-reauth', 'inherit']) + self.cli_commit() + + swanctl_conf = read_file(swanctl_file) + self.assertIn(f'reauth_time = {ike_lifetime}s', swanctl_conf) + + # ikev2-reauth = yes on peer overrides group + self.cli_delete(base_path + ['ike-group', ike_group, 'ikev2-reauth']) + self.cli_set(peer_base_path + ['ikev2-reauth', 'yes']) + self.cli_commit() + + swanctl_conf = read_file(swanctl_file) + self.assertIn(f'reauth_time = {ike_lifetime}s', swanctl_conf) + + # ikev2-reauth = no suppresses group flag + self.cli_set(base_path + ['ike-group', ike_group, 'ikev2-reauth']) + self.cli_set(peer_base_path + ['ikev2-reauth', 'no']) + self.cli_commit() + + swanctl_conf = read_file(swanctl_file) + self.assertNotIn(f'reauth_time = {ike_lifetime}s', swanctl_conf) + + # connection-type trap: reauth must be suppressed + self.cli_set(peer_base_path + ['connection-type', 'trap']) + self.cli_set(peer_base_path + ['ikev2-reauth', 'yes']) + self.cli_commit() + + swanctl_conf = read_file(swanctl_file) + self.assertNotIn(f'reauth_time = {ike_lifetime}s', swanctl_conf) + self.assertIn('keyingtries = 1', swanctl_conf) + def test_flex_vpn_vips(self): local_address = '192.0.2.5' @@ -915,10 +1441,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): for line in swanctl_lines: self.assertIn(line, swanctl_conf) - swanctl_unexpected_lines = [ - f'auth = eap-', - f'eap_id' - ] + swanctl_unexpected_lines = [f'auth = eap-', f'eap_id', f'send_cert ='] for unexpected_line in swanctl_unexpected_lines: self.assertNotIn(unexpected_line, swanctl_conf) @@ -935,6 +1458,22 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{int_ca_name}.pem'))) self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem'))) + # Add the always-send-cert config and observe the change + self.cli_set( + base_path + + [ + 'remote-access', + 'connection', + conn_name, + 'authentication', + 'always-send-cert', + ] + ) + self.cli_commit() + + swanctl_conf = read_file(swanctl_file) + self.assertIn(f'send_cert = always', swanctl_conf) + self.tearDownPKI() def test_remote_access_dhcp_fail_handling(self): @@ -1005,7 +1544,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.tearDownPKI() def test_remote_access_no_rekey(self): - # In some RA secnarios, disabling server-initiated rekey of IKE and CHILD SA is desired + # In some RA scenarios, disabling server-initiated rekey of IKE and CHILD SA is desired self.setupPKI() ike_group = 'IKE-RW' @@ -1380,5 +1919,51 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.tearDownPKI() + def test_retransmission_settings(self): + retransmit_base = '2.2' + retransmit_timeout = '10' + retransmit_attempts = '8' + self.cli_set(base_path + ['options', 'retransmission', 'base', retransmit_base]) + self.cli_set(base_path + ['options', 'retransmission', 'timeout', retransmit_timeout]) + self.cli_set(base_path + ['options', 'retransmission', 'attempts', retransmit_attempts]) + + self.cli_commit() + + # Verify charon configuration + charon_conf = read_file(charon_file) + charon_conf_lines = [ + f'# IKEv2 RETRANSMISSION', + f'retransmit_tries = {retransmit_attempts}', + f'retransmit_base = {retransmit_base}', + f'retransmit_timeout = {retransmit_timeout}', + ] + + for line in charon_conf_lines: + self.assertIn(line, charon_conf) + + def test_retransmission_default_settings(self): + # config file to cli options correspondence + retransmission_options = { + 'retransmit_base' : 'base', + 'retransmit_timeout': 'timeout', + 'retransmit_tries': 'attempts', + } + + # commit changes + self.cli_commit() + + for config_option, cli_option in retransmission_options.items(): + # Check configured value against CLI default value + config_values_list = get_config_value(charon_file,config_option + ' =') + + if config_values_list: + config_value = config_values_list[0] + else: + config_value = None + cli_value = default_value(base_path + ['options', 'retransmission', cli_option]) + self.assertEqual(config_value, cli_value) + + + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_vpn_l2tp.py b/smoketest/scripts/cli/test_vpn_l2tp.py index 07a7e2906..6e7959056 100755 --- a/smoketest/scripts/cli/test_vpn_l2tp.py +++ b/smoketest/scripts/cli/test_vpn_l2tp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,9 +17,13 @@ import unittest from base_accel_ppp_test import BasicAccelPPPTest +from base_vyostest_shim import VyOSUnitTestSHIM + from configparser import ConfigParser from vyos.utils.process import cmd +from vyos.utils.file import read_file +swanctl_file = '/etc/swanctl/swanctl.conf' class TestVPNL2TPServer(BasicAccelPPPTest.TestCase): @classmethod @@ -57,11 +61,16 @@ class TestVPNL2TPServer(BasicAccelPPPTest.TestCase): def test_vpn_l2tp_dependence_ipsec_swanctl(self): # Test config vpn for tasks T3843 and T5926 + outside_address = '203.0.113.1' + base_path = ['vpn', 'l2tp', 'remote-access'] # make precondition self.cli_set(['interfaces', 'dummy', 'dum0', 'address', '203.0.113.1/32']) self.cli_set(['vpn', 'ipsec', 'interface', 'dum0']) + # Passing the 'unique = never' for StrongSwan's `connections.<conn>.unique` parameter + self.cli_set(['vpn', 'ipsec', 'disable-uniqreqids']) + self.cli_commit() # check ipsec apply to swanctl self.assertEqual('', cmd('echo vyos | sudo -S swanctl -L ')) @@ -76,7 +85,7 @@ class TestVPNL2TPServer(BasicAccelPPPTest.TestCase): self.cli_set(base_path + ['ipsec-settings', 'authentication', 'pre-shared-secret', 'SeCret']) self.cli_set(base_path + ['ipsec-settings', 'ike-lifetime', '8600']) self.cli_set(base_path + ['ipsec-settings', 'lifetime', '3600']) - self.cli_set(base_path + ['outside-address', '203.0.113.1']) + self.cli_set(base_path + ['outside-address', outside_address]) self.cli_set(base_path + ['gateway-address', '203.0.113.1']) self.cli_commit() @@ -84,6 +93,19 @@ class TestVPNL2TPServer(BasicAccelPPPTest.TestCase): # check l2tp apply to swanctl self.assertTrue('l2tp_remote_access:' in cmd('echo vyos | sudo -S swanctl -L ')) + swanctl_conf = read_file(swanctl_file) + swanctl_lines = [ + f'local_addrs = {outside_address}', + 'proposals = aes256-sha1-modp1024,3des-sha1-modp1024', + 'dpd_delay = 15s', + 'dpd_timeout = 45s', + 'rekey_time = 8600s', + 'reauth_time = 0', + 'unique = never', + ] + for line in swanctl_lines: + self.assertIn(line, swanctl_conf) + self.cli_delete(['vpn', 'l2tp']) self.cli_commit() @@ -120,4 +142,4 @@ class TestVPNL2TPServer(BasicAccelPPPTest.TestCase): if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_vpn_openconnect.py b/smoketest/scripts/cli/test_vpn_openconnect.py index dcce229e2..6ca4295c1 100755 --- a/smoketest/scripts/cli/test_vpn_openconnect.py +++ b/smoketest/scripts/cli/test_vpn_openconnect.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -166,11 +166,12 @@ class TestVPNOpenConnect(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.assertTrue(process_named_running(PROCESS_NAME)) - self.cli_delete(base_path) self.cli_commit() - + # Check for no longer running process self.assertFalse(process_named_running(PROCESS_NAME)) + # always forward to base class + super().tearDown() def test_ocserv(self): user = 'vyos_user' @@ -265,4 +266,4 @@ class TestVPNOpenConnect(VyOSUnitTestSHIM.TestCase): self.assertIn('tls-priorities = "NORMAL:%SERVER_PRECEDENCE:%COMPAT:-RSA:-VERS-SSL3.0:-ARCFOUR-128:-VERS-TLS1.0:-VERS-TLS1.1:-VERS-TLS1.2"', daemon_config) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_vpn_pptp.py b/smoketest/scripts/cli/test_vpn_pptp.py index 25d9a4760..6985454cf 100755 --- a/smoketest/scripts/cli/test_vpn_pptp.py +++ b/smoketest/scripts/cli/test_vpn_pptp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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_accel_ppp_test import BasicAccelPPPTest +from base_vyostest_shim import VyOSUnitTestSHIM class TestVPNPPTPServer(BasicAccelPPPTest.TestCase): @classmethod @@ -36,4 +37,4 @@ class TestVPNPPTPServer(BasicAccelPPPTest.TestCase): pass if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_vpn_sstp.py b/smoketest/scripts/cli/test_vpn_sstp.py index 1a3e1df6e..14114fd77 100755 --- a/smoketest/scripts/cli/test_vpn_sstp.py +++ b/smoketest/scripts/cli/test_vpn_sstp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,8 @@ import unittest from base_accel_ppp_test import BasicAccelPPPTest +from base_vyostest_shim import VyOSUnitTestSHIM + from vyos.utils.file import read_file pki_path = ['pki'] @@ -85,6 +87,5 @@ class TestVPNSSTPServer(BasicAccelPPPTest.TestCase): config = read_file(self._config_file) self.assertIn(f'host-name={host_name}', config) - if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_vpp.py b/smoketest/scripts/cli/test_vpp.py new file mode 100755 index 000000000..a506ad4f0 --- /dev/null +++ b/smoketest/scripts/cli/test_vpp.py @@ -0,0 +1,1518 @@ +#!/usr/bin/env python3 +# +# Copyright (C) VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os +import re +import unittest +from collections import defaultdict + +from json import loads + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSessionError +from vyos.utils.convert import range_str_to_list +from vyos.utils.convert import list_to_range_str +from vyos.utils.process import process_named_running +from vyos.utils.file import read_file +from vyos.utils.process import rc_cmd +from vyos.utils.system import sysctl_read +from vyos.utils.network import interface_exists +from vyos.system import image +from vyos.vpp import VPPControl +from vyos.vpp.utils import vpp_iface_name_transform +from vyos.vpp.config_resource_checks.resource_defaults import default_resource_map + +PROCESS_NAME = 'vpp_main' +VPP_CONF = '/run/vpp/vpp.conf' +base_path = ['vpp'] +resource_path = base_path + ['settings', 'resource-allocation'] +interfaces_path = ['interfaces', 'vpp'] +interface = 'eth1' + + +def get_vpp_config(): + config = defaultdict(dict) + current_section = None + + with open(VPP_CONF, 'r') as f: + for line in f: + line = line.strip() + + if not line or line.startswith('#'): # Ignore empty lines and comments + continue + + section_match = re.match(r'([a-zA-Z0-9_-]+)\s*{', line) + if section_match: + current_section = section_match.group(1) + config[current_section] = {} + continue + + if line == '}': # End of section + current_section = None + continue + + key_value_match = re.match(r'([a-zA-Z0-9_-]+)\s+(.+)', line) + if key_value_match: + key, value = key_value_match.groups() + if current_section: + config[current_section][key] = value + else: + config[key] = value + + return config + + +def get_address(interface): + rc, data = rc_cmd(f'ip --json address show dev {interface}') + if rc == 0: + data = loads(data) + if isinstance(data, list) and len(data) > 0: + ip_address = data[0]['addr_info'][0]['local'] + return ip_address + + +def get_isolated_cpus(): + isolated = read_file('/sys/devices/system/cpu/isolated') + return range_str_to_list(isolated) + + +class TestVPP(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(TestVPP, 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, interfaces_path) + + def setUp(self): + # always forward to base class + super().setUp() + + self.cli_set(base_path + ['settings', 'interface', interface]) + self.cli_set(base_path + ['settings', 'poll-sleep-usec', '10']) + + def tearDown(self): + try: + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + finally: + # Ensure these cleanup operations always run + self.cli_delete(base_path) + self.cli_delete(interfaces_path) + self.cli_commit() + + # delete address for Ethernet interface + self.cli_delete(['interfaces', 'ethernet', interface, 'address']) + self.cli_commit() + + self.assertFalse(os.path.exists(VPP_CONF)) + self.assertFalse(process_named_running(PROCESS_NAME)) + # always forward to base class + super().tearDown() + + def test_01_vpp_basic(self): + poll_sleep = '0' + mtu = '2500' + isolated_cores = get_isolated_cpus() + + self.cli_set(base_path + ['settings', 'poll-sleep-usec', poll_sleep]) + + # commit changes + self.cli_commit() + + config_entries = ( + f'poll-sleep-usec {poll_sleep}', + f'main-core {str(isolated_cores[0])}', # first isolated core is set as main-core + 'plugin default { disable }', + 'plugin dpdk_plugin.so { enable }', + 'plugin linux_cp_plugin.so { enable }', + 'plugin dhcp_plugin.so { enable }', + 'dev 0000:00:00.0', + 'uio-bind-force', + ) + + # Check configured options + config = read_file(VPP_CONF) + for config_entry in config_entries: + self.assertIn(config_entry, config) + + # route-no-paths is not present in the output + # looks like vpp bug + _, out = rc_cmd('sudo vppctl show lcp') + required_str = 'lcp route-no-paths on' + self.assertIn(required_str, out) + + self.cli_set(base_path + ['settings', 'ignore-kernel-routes']) + self.cli_commit() + + # check disabled 'route no path' + _, out = rc_cmd('sudo vppctl show lcp') + required_str = 'lcp route-no-paths off' + self.assertIn(required_str, out) + + # set interface MTU + self.cli_set(['interfaces', 'ethernet', interface, 'mtu', mtu]) + self.cli_commit() + + # check MTU for the LCP interface pair + _, out = rc_cmd('sudo vppctl show interface') + normalized_out = re.sub(r'\s+', ' ', out) + self.assertIn(f'tap4096 2 up {mtu}/0/0/0', normalized_out) + + # delete mtu settings + self.cli_delete(['interfaces', 'ethernet', interface, 'mtu']) + self.cli_commit() + + # set interface address as dhcp + self.cli_set(['interfaces', 'ethernet', interface, 'address', 'dhcp']) + self.cli_commit() + + vpp = VPPControl() + + # check 'ip4-dhcp-client-detect' feature is enabled on interface + client_detect_feature = vpp.api.feature_is_enabled( + sw_if_index=vpp.get_sw_if_index(interface), + feature_name='ip4-dhcp-client-detect', + arc_name='ip4-unicast', + ) + self.assertTrue(client_detect_feature.is_enabled) + + # set interface address as dhcpv6 + self.cli_set(['interfaces', 'ethernet', interface, 'address', 'dhcpv6']) + self.cli_commit() + + # check 'ip6-icmp-ra-punt' feature is enabled on interface + # for ip6-unicast and ip6-multicast arcs + for arc_name in ['ip6-unicast', 'ip6-multicast']: + icmpv6_ra_punt_feature = vpp.api.feature_is_enabled( + sw_if_index=vpp.get_sw_if_index(interface), + feature_name='ip6-icmp-ra-punt', + arc_name=arc_name, + ) + self.assertTrue(icmpv6_ra_punt_feature.is_enabled) + + def test_02_vpp_vxlan(self): + vxlan_path = interfaces_path + ['vxlan'] + vni = '23' + interface_vxlan = f'vppvxlan{vni}' + source_address = '192.0.2.1' + new_source_address = '192.0.2.3' + remote_address = '192.0.2.254' + address = '203.0.113.1' + + self.cli_set(['interfaces', 'ethernet', interface, 'address', '192.0.2.1/24']) + self.cli_set(vxlan_path + [interface_vxlan, 'source-address', source_address]) + self.cli_set(vxlan_path + [interface_vxlan, 'vni', vni]) + + # remote and source address must not be the same + # expect raise ConfigError + self.cli_set(vxlan_path + [interface_vxlan, 'remote', source_address]) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(vxlan_path + [interface_vxlan, 'remote', remote_address]) + self.cli_set(vxlan_path + [interface_vxlan, 'address', f'{address}/24']) + + # commit changes + self.cli_commit() + + self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_vxlan}')) + + current_address = get_address(interface_vxlan) + self.assertEqual(address, current_address) + + # check vxlan interface + _, out = rc_cmd('sudo vppctl show vxlan tunnel') + required_str = f'[0] instance 23 src {source_address} dst {remote_address} src_port 4789 dst_port 4789 vni {vni}' + self.assertIn(required_str, out) + + # update vxlan interface + self.cli_set( + vxlan_path + [interface_vxlan, 'source-address', new_source_address] + ) + + # source address of the tunnel interface should be configured + # expect raise ConfigError + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + [ + 'interfaces', + 'ethernet', + interface, + 'vif', + vni, + 'address', + f'{new_source_address}/24', + ] + ) + self.cli_commit() + + # check gre interface after update + _, out = rc_cmd('sudo vppctl show vxlan tunnel') + required_str = ( + f'[0] instance {vni} src {new_source_address} dst {remote_address}' + ) + self.assertIn(required_str, out) + self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_vxlan}')) + self.assertEqual(address, current_address) + + # change vpp settings + self.cli_set(base_path + ['settings', 'poll-sleep-usec', '5']) + self.cli_commit() + + config = read_file(VPP_CONF) + self.assertIn('poll-sleep-usec 5', config) + + # delete vxlan interface + self.cli_delete(vxlan_path + [interface_vxlan]) + self.cli_commit() + + # delete vif Ethernet interface + self.cli_delete(['interfaces', 'ethernet', interface, 'vif']) + self.cli_commit() + + def test_03_vpp_gre(self): + gre_path = interfaces_path + ['gre'] + interface_gre = 'vppgre12' + source_address = '192.0.2.1' + new_source_address = '192.0.2.2' + remote_address = '192.0.2.254' + address = '10.0.0.0' + + self.cli_set(gre_path + [interface_gre, 'source-address', source_address]) + self.cli_set(gre_path + [interface_gre, 'remote', remote_address]) + self.cli_set(gre_path + [interface_gre, 'address', f'{address}/31']) + + # source address of the tunnel interface should be configured + # expect raise ConfigError + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + ['interfaces', 'ethernet', interface, 'address', f'{source_address}/24'] + ) + + # commit changes + self.cli_commit() + + self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_gre}')) + current_address = get_address(interface_gre) + self.assertEqual(address, current_address) + + # check gre interface + _, out = rc_cmd('sudo vppctl show gre tunnel') + required_str = f'[0] instance 12 src {source_address} dst {remote_address}' + self.assertIn(required_str, out) + + # update gre interface + self.cli_set(gre_path + [interface_gre, 'source-address', new_source_address]) + + self.cli_set( + ['interfaces', 'ethernet', interface, 'address', f'{new_source_address}/24'] + ) + self.cli_commit() + + # check gre interface after update + _, out = rc_cmd('sudo vppctl show gre tunnel') + required_str = f'[0] instance 12 src {new_source_address} dst {remote_address}' + self.assertIn(required_str, out) + self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_gre}')) + self.assertEqual(address, current_address) + + # delete gre interface + self.cli_delete(gre_path + [interface_gre]) + self.cli_commit() + + def test_04_vpp_loopback(self): + loopback_path = interfaces_path + ['loopback'] + interface_loopback = 'vpplo11' + address = '192.0.2.54' + + self.cli_set(loopback_path + [interface_loopback]) + self.cli_set(loopback_path + [interface_loopback, 'address', f'{address}/25']) + + # commit changes + self.cli_commit() + + self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_loopback}')) + + current_address = get_address(interface_loopback) + self.assertEqual(address, current_address) + + # check loopback interface + _, out = rc_cmd('sudo vppctl show interface loop11') + required_str = 'loop11' + self.assertIn(required_str, out) + + # delete loopback interface + self.cli_delete(loopback_path + [interface_loopback]) + self.cli_commit() + + def test_05_vpp_bonding(self): + bond_path = interfaces_path + ['bonding'] + interface_bond = 'vppbond23' + hash = 'layer3+4' + mode = '802.3ad' + description = 'Interface-Bonding' + vlans = ['123', '456'] + vlan_description = 'My-vlan-123' + + self.cli_set(bond_path + [interface_bond, 'member', 'interface', interface]) + self.cli_set(bond_path + [interface_bond, 'hash-policy', hash]) + self.cli_set(bond_path + [interface_bond, 'mode', mode]) + + # commit changes + self.cli_commit() + + # Check for interface state "BondEthernet23 up" + _, out = rc_cmd('sudo vppctl show interface') + # Normalize the output for consistent whitespace + normalized_out = re.sub(r'\s+', ' ', out) + self.assertRegex( + normalized_out, + r'BondEthernet23\s+\d+\s+up', + "Interface BondEthernet23 is not in the expected state 'up'.", + ) + + self.cli_set(bond_path + [interface_bond, 'description', description]) + for vlan in vlans: + self.cli_set( + bond_path + + [interface_bond, 'vif', vlan, 'description', vlan_description] + ) + + # commit changes + self.cli_commit() + + self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_bond}')) + self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_bond}.{vlan}')) + + current_alias = read_file(f'/sys/class/net/{interface_bond}/ifalias') + vlan_alias = read_file(f'/sys/class/net/{interface_bond}.{vlan}/ifalias') + self.assertEqual(current_alias, description) + self.assertEqual(vlan_alias, vlan_description) + + # check bonding interface + _, out = rc_cmd('sudo vppctl show bond details') + required_enries = ( + 'BondEthernet23', + 'mode: lacp', + 'load balance: l34', + 'number of active members: 0', + 'number of members: 1', + f'{interface}', + 'device instance: 0', + 'interface id: 23', + ) + for entry in required_enries: + self.assertIn(entry, out) + + # check interface state + _, out = rc_cmd('sudo vppctl show interface') + # Normalize the output for consistent whitespace + normalized_out = re.sub(r'\s+', ' ', out) + # Check for interface state "BondEthernet23 up" + self.assertRegex( + normalized_out, + r'BondEthernet23\s+\d+\s+up', + "Interface BondEthernet23 is not in the expected state 'up'.", + ) + + # delete vpp interface vlan + self.cli_delete(bond_path + [interface_bond, 'vif']) + self.cli_commit() + self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_bond}.{vlan}')) + + # delete bonding interface + self.cli_delete(bond_path) + self.cli_commit() + + # check deleting bonding interface + _, out = rc_cmd('sudo vppctl show interface') + self.assertNotIn('BondEthernet23', out) + + def test_06_vpp_bridge(self): + bridge_path = interfaces_path + ['bridge'] + fake_member = 'eth2' + members = [interface] + interface_bridge = 'vppbr10' + vni = '23' + interface_vxlan = f'vppvxlan{vni}' + source_address = '192.0.2.1' + remote_address = '192.0.2.254' + + self.cli_set(['interfaces', 'ethernet', interface, 'address', '192.0.2.1/24']) + for member in members: + self.cli_set( + bridge_path + [interface_bridge, 'member', 'interface', member] + ) + + # commit changes + self.cli_commit() + + # check bridge interface + _, out = rc_cmd('sudo vppctl show bridge-domain 10 detail') + + # Normalize the output for consistent whitespace + normalized_out = re.sub(r'\s+', ' ', out) + + # Perform assertions based on the normalized output + self.assertIn('BD-ID Index BSN Age(min)', normalized_out) + self.assertIn('10 1 0 off', normalized_out) + self.assertIn('Learning U-Forwrd UU-Flood Flooding', normalized_out) + self.assertIn('on on flood on', normalized_out) + self.assertIn('Interface If-idx ISN', normalized_out) + # Check Interface, If-idx, ISN + self.assertRegex(out, r'\s*eth1\s+\d+\s+\d+') + + # Set non exist member + # expect raise ConfigError + self.cli_set( + bridge_path + [interface_bridge, 'member', 'interface', fake_member] + ) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete( + bridge_path + [interface_bridge, 'member', 'interface', fake_member] + ) + + # Add VXLAN to the bridge + self.cli_set( + interfaces_path + + ['vxlan', interface_vxlan, 'source-address', source_address] + ) + self.cli_set( + interfaces_path + ['vxlan', interface_vxlan, 'remote', remote_address] + ) + self.cli_set(interfaces_path + ['vxlan', interface_vxlan, 'vni', vni]) + self.cli_set( + bridge_path + [interface_bridge, 'member', 'interface', interface_vxlan] + ) + + # commit changes + self.cli_commit() + + # check bridge interface + _, out = rc_cmd('sudo vppctl show bridge-domain 10 detail') + # Normalize the output for consistent whitespace + normalized_out = re.sub(r'\s+', ' ', out) + + # Perform assertions based on the normalized output + self.assertIn('BD-ID Index BSN Age(min)', normalized_out) + self.assertRegex(normalized_out, r'10 1 \d+ off') + self.assertIn('Learning U-Forwrd UU-Flood Flooding', normalized_out) + self.assertIn('on on flood on', normalized_out) + self.assertIn('Interface If-idx ISN', normalized_out) + # Check Interface, If-idx, ISN + self.assertRegex(out, r'\s*eth1\s+\d+\s+\d+') + self.assertRegex(out, r'\s*vxlan_tunnel23\s+\d+\s+\d+') + + # Add check dependency ethernet => bridge + self.cli_set( + base_path + ['settings', 'interface', interface, 'num-rx-desc', '512'] + ) + self.cli_commit() + # check bridge interface + _, out = rc_cmd('sudo vppctl show bridge-domain 10 detail') + # Normalize the output for consistent whitespace + normalized_out = re.sub(r'\s+', ' ', out) + self.assertRegex(out, r'\s*eth1\s+\d+\s+\d+') + self.assertRegex(out, r'\s*vxlan_tunnel23\s+\d+\s+\d+') + + # Cannot add members of bridge interface to cross-connect + # expect raise ConfigError + self.cli_set( + interfaces_path + ['xconnect', 'vppxcon1', 'member', 'interface', interface] + ) + self.cli_set( + interfaces_path + + ['xconnect', 'vppxcon1', 'member', 'interface', interface_vxlan] + ) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(interfaces_path + ['xconnect']) + + # Add Loopback BVI to the bridge + self.cli_set(interfaces_path + ['loopback', f'vpplo{vni}']) + self.cli_set( + bridge_path + + [interface_bridge, 'member', 'interface', f'vpplo{vni}', 'bvi'] + ) + # commit changes + self.cli_commit() + + # check bridge interface + _, out = rc_cmd('sudo vppctl show bridge-domain 10 detail') + # Normalize the output for consistent whitespace + normalized_out = re.sub(r'\s+', ' ', out) + + self.assertRegex(normalized_out, r'10 1 \d+ off') + self.assertRegex(out, r'\bloop23\s+\d+\s+\d+\s+\d+\s+\*\s+') + + def test_07_vpp_ipip(self): + ipip_path = interfaces_path + ['ipip'] + interface_ipip = 'vppipip12' + source_address = '192.0.2.1' + new_source_address = '192.0.2.2' + remote_address = '192.0.2.5' + address = '10.0.0.0' + + self.cli_set(['interfaces', 'ethernet', interface, 'address', '192.0.2.1/24']) + self.cli_set(ipip_path + [interface_ipip, 'source-address', source_address]) + self.cli_set(ipip_path + [interface_ipip, 'remote', remote_address]) + self.cli_set(ipip_path + [interface_ipip, 'address', f'{address}/31']) + + # commit changes + self.cli_commit() + + self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_ipip}')) + current_address = get_address(interface_ipip) + self.assertEqual(address, current_address) + + # check ipip interface + _, out = rc_cmd('sudo vppctl show ipip tunnel') + required_str = f'[0] instance 12 src {source_address} dst {remote_address}' + self.assertIn(required_str, out) + + # update ipip interface + self.cli_set(ipip_path + [interface_ipip, 'source-address', new_source_address]) + + # source address of the tunnel interface should be configured + # expect raise ConfigError + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + ['interfaces', 'ethernet', interface, 'address', f'{new_source_address}/24'] + ) + self.cli_commit() + + # check ipip interface after update + _, out = rc_cmd('sudo vppctl show ipip tunnel') + required_str = f'[0] instance 12 src {new_source_address} dst {remote_address}' + self.assertIn(required_str, out) + self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_ipip}')) + self.assertEqual(address, current_address) + + # delete ipip interface + self.cli_delete(ipip_path + [interface_ipip]) + self.cli_commit() + + def test_08_vpp_xconnect(self): + xconn_path = interfaces_path + ['xconnect'] + vni = '23' + interface_vxlan = f'vppvxlan{vni}' + interface_xconnect = f'vppxcon{vni}' + source_address = '192.0.2.1' + remote_address = '192.0.2.254' + + self.cli_set(['interfaces', 'ethernet', interface, 'address', '192.0.2.1/24']) + self.cli_set( + interfaces_path + + ['vxlan', interface_vxlan, 'source-address', source_address] + ) + self.cli_set( + interfaces_path + ['vxlan', interface_vxlan, 'remote', remote_address] + ) + self.cli_set(interfaces_path + ['vxlan', interface_vxlan, 'vni', vni]) + + # Add xconneect + self.cli_set( + xconn_path + [interface_xconnect, 'member', 'interface', interface] + ) + + # Cross connect interfaces require 2 interfaces + # expect raise ConfigError + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + xconn_path + [interface_xconnect, 'member', 'interface', interface_vxlan] + ) + + # commit changes + self.cli_commit() + + # check interface mode + _, out = rc_cmd('sudo vppctl show mode') + required_str_list = [ + f'l2 xconnect {interface} vxlan_tunnel{vni}', + f'l2 xconnect vxlan_tunnel{vni} {interface}', + ] + for required_string in required_str_list: + self.assertIn(required_string, out) + + # Cannot add members of cross-connect interface to bond/bridge + # expect raise ConfigError + self.cli_set( + interfaces_path + + ['bonding', 'vppbond1', 'member', 'interface', interface_vxlan] + ) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(interfaces_path + ['bonding']) + + # delete xconnect interface + self.cli_delete(xconn_path + [interface_xconnect]) + self.cli_commit() + + # check delete xconnect interface + _, out = rc_cmd('sudo vppctl show mode') + for required_string in required_str_list: + self.assertNotIn(required_string, out) + + def test_09_vpp_driver_options(self): + driver_options = { + 'num-rx-desc': '512', + 'num-tx-desc': '512', + 'num-rx-queues': '2', + 'num-tx-queues': '2', + } + cpu_cores = '2' + + base_interface_path = base_path + ['settings', 'interface', interface] + + for option, value in driver_options.items(): + self.cli_set(base_interface_path + [option, value]) + + # rx/tx queue configuration expect VPP workers to be set + # expect raise ConfigError + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(resource_path + ['cpu-cores', cpu_cores]) + + # # DPDK driver expect only dpdk-options and not xdp-options to be set + # # expect raise ConfigError + # self.cli_set(base_interface_path + ['xdp-options', 'zero-copy']) + # + # with self.assertRaises(ConfigSessionError): + # self.cli_commit() + # + # # delete xdp-options and apply commit + # self.cli_delete(base_interface_path + ['xdp-options']) + + self.cli_commit() + + # check dpdk options in config file + config = read_file(VPP_CONF) + + for option, value in driver_options.items(): + self.assertIn(f'{option} {value}', config) + + def test_10_vpp_cpu_cores(self): + cpu_cores = '2' + isolated_cpus = get_isolated_cpus() + main_core = str(isolated_cpus[0]) # first isolated core is set as main-core + corelist_workers = list_to_range_str(isolated_cpus[1 : int(cpu_cores)]) + + # verify 'cpu-cores' are set not correctly + # expect raise ConfigError + self.cli_set(resource_path + ['cpu-cores', '99']) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(resource_path + ['cpu-cores', cpu_cores]) + self.cli_commit() + + config_entries = ( + f'main-core {main_core}', + f'corelist-workers {corelist_workers}', + 'dev 0000:00:00.0', + ) + + # Check configured options + config = read_file(VPP_CONF) + for config_entry in config_entries: + self.assertIn(config_entry, config) + + def test_11_1_buffer_page_size(self): + sizes = ['4K', '2M'] + for size in sizes: + self.cli_set(resource_path + ['buffers', 'page-size', size]) + self.cli_commit() + + conf = get_vpp_config() + self.assertEqual(conf['buffers']['page-size'], size) + + def test_11_2_statseg_page_size(self): + sizes = ['4K', '2M'] + for size in sizes: + self.cli_set(resource_path + ['memory', 'stats', 'page-size', size]) + self.cli_commit() + + conf = get_vpp_config() + self.assertEqual(conf['statseg']['page-size'], size) + + def test_11_3_mem_page_size(self): + sizes = ['4K', '2M'] + for size in sizes: + self.cli_set(resource_path + ['memory', 'main-heap-page-size', size]) + self.cli_commit() + + conf = get_vpp_config() + self.assertEqual(conf['memory']['main-heap-page-size'], size) + + def test_12_vpp_ipsec_xfrm_nl(self): + rx_buffer_zise = default_resource_map.get('netlink_rx_buffer_size') + + self.cli_set(base_path + ['settings', 'ipsec-acceleration']) + self.cli_commit() + + config_entries = ( + 'linux-xfrm-nl', + 'enable-route-mode-ipsec', + 'interface ipsec', + f'nl-rx-buffer-size {rx_buffer_zise}', + ) + + # Check configured options + config = read_file(VPP_CONF) + for config_entry in config_entries: + self.assertIn(config_entry, config) + + def test_13_1_vpp_cgnat(self): + base_cgnat = base_path + ['nat', 'cgnat'] + iface_out = 'eth0' + iface_inside = 'eth1' + timeout_udp = '150' + timeout_icmp = '30' + timeout_tcp_est = '600' + timeout_tcp_trans = '120' + inside_prefix = '100.64.0.0/24' + outside_prefix = '192.0.2.1/32' + + self.cli_set(base_path + ['settings', 'interface', iface_out]) + self.cli_set(base_cgnat + ['interface', 'inside', iface_inside]) + self.cli_set(base_cgnat + ['interface', 'outside', iface_out]) + self.cli_set(base_cgnat + ['rule', '100', 'inside-prefix', inside_prefix]) + self.cli_set(base_cgnat + ['rule', '100', 'outside-prefix', outside_prefix]) + self.cli_set(base_cgnat + ['timeout', 'icmp', timeout_icmp]) + self.cli_set(base_cgnat + ['timeout', 'tcp-established', timeout_tcp_est]) + self.cli_set(base_cgnat + ['timeout', 'tcp-transitory', timeout_tcp_trans]) + self.cli_set(base_cgnat + ['timeout', 'udp', timeout_udp]) + self.cli_commit() + + # Check interfaces + _, out = rc_cmd('sudo vppctl show det44 interfaces') + self.assertIn(f'{iface_inside} in', out) + self.assertIn(f'{iface_out} out', out) + + # Check mappings + _, out = rc_cmd('sudo vppctl show det44 mappings') + self.assertIn(inside_prefix, out) + self.assertIn(outside_prefix, out) + + # Check timeouts + _, out = rc_cmd('sudo vppctl show det44 timeouts') + self.assertIn(f'udp timeout: {timeout_udp}sec', out) + self.assertIn(f'tcp established timeout: {timeout_tcp_est}sec', out) + self.assertIn(f'tcp transitory timeout: {timeout_tcp_trans}sec', out) + self.assertIn(f'icmp timeout: {timeout_icmp}sec', out) + + def test_13_2_vpp_cgnat_bond_with_vifs(self): + base_cgnat = base_path + ['nat', 'cgnat'] + base_bond = interfaces_path + ['bonding'] + iface_bond = 'vppbond0' + vif_1 = '23' + vif_2 = '24' + iface_out = f'{iface_bond}.{vif_1}' + iface_inside = f'{iface_bond}.{vif_2}' + address_1 = '100.64.0.23/32' + address_2 = '192.0.2.1/32' + + self.cli_set(base_bond + [iface_bond, 'member', 'interface', interface]) + self.cli_set(base_bond + [iface_bond, 'vif', vif_1, 'address', address_1]) + self.cli_set(base_bond + [iface_bond, 'vif', vif_2, 'address', address_2]) + + self.cli_set(base_cgnat + ['interface', 'inside', iface_inside]) + self.cli_set(base_cgnat + ['interface', 'outside', iface_out]) + self.cli_set(base_cgnat + ['rule', '100', 'inside-prefix', address_1]) + self.cli_set(base_cgnat + ['rule', '100', 'outside-prefix', address_2]) + self.cli_commit() + + # Check interfaces + _, out = rc_cmd('sudo vppctl show det44 interfaces') + self.assertIn(f'BondEthernet0.{vif_2} in', out) + self.assertIn(f'BondEthernet0.{vif_1} out', out) + + # Change bonding interface configuration + self.cli_set(base_bond + [iface_bond, 'mode', '802.3ad']) + self.cli_commit() + + # Check interfaces + _, out = rc_cmd('sudo vppctl show det44 interfaces') + self.assertIn(f'BondEthernet0.{vif_2} in', out) + self.assertIn(f'BondEthernet0.{vif_1} out', out) + + # Verify only expected interfaces are shown: + # header + inside + outside = 3 lines total + lines = out.split('\n') + self.assertTrue(len(lines) == 3) + + # Cannot remove inside/outside interface from vpp while it is used in the feature + # expect raise ConfigError + self.cli_delete(base_bond + [iface_bond, 'vif', vif_1]) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_discard() + + def test_14_vpp_nat44(self): + base_nat = base_path + ['nat', 'nat44'] + exclude_local_addr = '100.64.0.52' + exclude_local_port = '22' + iface_out = 'eth0' + iface_inside = 'eth1' + timeout_udp = '150' + timeout_icmp = '30' + timeout_tcp_est = '600' + timeout_tcp_trans = '120' + translation_pool = '192.0.2.1-192.0.2.2' + static_ext_addr = '192.0.2.55' + static_local_addr = '100.64.0.55' + sess_limit = '64000' + + self.cli_set(base_path + ['settings', 'interface', iface_out]) + self.cli_set(base_nat + ['interface', 'inside', iface_inside]) + self.cli_set(base_nat + ['interface', 'outside', iface_out]) + self.cli_set( + base_nat + ['address-pool', 'translation', 'address', translation_pool] + ) + self.cli_commit() + + # Forwarding is disabled when only dynamic NAT is configured + vpp = VPPControl() + out = vpp.api.nat44_show_running_config().forwarding_enabled + self.assertFalse(out) + + self.cli_set( + base_nat + ['exclude', 'rule', '100', 'local-address', exclude_local_addr] + ) + self.cli_set( + base_nat + ['exclude', 'rule', '100', 'local-port', exclude_local_port] + ) + + # cannot set local-port without specifying protocol + # expect raise ConfigError + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(base_nat + ['exclude', 'rule', '100', 'protocol', 'tcp']) + self.cli_set( + base_nat + ['static', 'rule', '100', 'external', 'address', static_ext_addr] + ) + self.cli_set( + base_nat + ['static', 'rule', '100', 'local', 'address', static_local_addr] + ) + + self.cli_set(base_nat + ['session-limit', sess_limit]) + self.cli_set(base_nat + ['timeout', 'icmp', timeout_icmp]) + self.cli_set(base_nat + ['timeout', 'tcp-established', timeout_tcp_est]) + self.cli_set(base_nat + ['timeout', 'tcp-transitory', timeout_tcp_trans]) + self.cli_set(base_nat + ['timeout', 'udp', timeout_udp]) + self.cli_commit() + + # Check addresses + _, out = rc_cmd('sudo vppctl show nat44 addresses') + self.assertIn(translation_pool.split('-')[0], out) + self.assertIn(translation_pool.split('-')[1], out) + + # Check interfaces + _, out = rc_cmd('sudo vppctl show nat44 interfaces') + self.assertIn(f'{iface_inside} in', out) + self.assertIn(f'{iface_out} out', out) + + # Check mappings + _, out = rc_cmd('sudo vppctl show nat44 static mappings') + self.assertIn( + f'local {static_local_addr} external {static_ext_addr} vrf 0', out + ) + self.assertIn(f'{exclude_local_addr}:{exclude_local_port} vrf 0', out) + + # Check timeouts + _, out = rc_cmd('sudo vppctl show nat timeouts') + self.assertIn(f'udp timeout: {timeout_udp}sec', out) + self.assertIn(f'tcp-established timeout: {timeout_tcp_est}sec', out) + self.assertIn(f'tcp-transitory timeout: {timeout_tcp_trans}sec', out) + self.assertIn(f'icmp timeout: {timeout_icmp}sec', out) + + # Summary + _, out = rc_cmd('sudo vppctl show nat44 summary') + self.assertIn(f'max translations per thread: {sess_limit} fib 0', out) + + # Forwarding should be disabled with statyc+dynamic NAT + vpp = VPPControl() + out = vpp.api.nat44_show_running_config().forwarding_enabled + self.assertFalse(out) + + # Delete dynamic NAT and check forwarding + self.cli_delete(base_nat + ['address-pool']) + self.cli_commit() + + # Forwarding should be enabled if only statyc NAT is configured + vpp = VPPControl() + out = vpp.api.nat44_show_running_config().forwarding_enabled + self.assertTrue(out) + + def test_15_vpp_sflow(self): + base_sflow = ['system', 'sflow'] + sampling_rate = '1500' + polling_interval = '55' + header_bytes = '256' + iface_2 = 'eth0' + + self.cli_set(base_path + ['sflow', 'interface', interface]) + self.cli_set(base_path + ['sflow', 'header-bytes', header_bytes]) + self.cli_set(base_sflow + ['interface', interface]) + self.cli_set(base_sflow + ['server', '127.0.0.1']) + self.cli_set(base_sflow + ['sampling-rate', sampling_rate]) + self.cli_set(base_sflow + ['polling', polling_interval]) + self.cli_set(base_sflow + ['vpp']) + self.cli_commit() + + # Check sFlow + _, out = rc_cmd('sudo vppctl show sflow') + + expected_entries = ( + f'sflow sampling-rate {sampling_rate}', + 'sflow direction rx', + f'sflow polling-interval {polling_interval}', + f'sflow header-bytes {header_bytes}', + f'sflow enable {interface}', + 'interfaces enabled: 1', + ) + + for expected_entry in expected_entries: + self.assertIn(expected_entry, out) + + self.cli_set(base_path + ['settings', 'interface', iface_2]) + self.cli_set(base_path + ['sflow', 'interface', iface_2]) + + self.cli_commit() + + # Check sFlow + _, out = rc_cmd('sudo vppctl show sflow') + + expected_entries = ( + f'sflow enable {interface}', + f'sflow enable {iface_2}', + 'interfaces enabled: 2', + ) + + for expected_entry in expected_entries: + self.assertIn(expected_entry, out) + + # Cannot remove interface from vpp while it is used in the feature + # expect raise ConfigError + self.cli_delete(base_path + ['settings', 'interface', iface_2]) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_discard() + + # cannot delete system sFlow configuration if VPP sFlow is configured + # expect raise ConfigError + self.cli_delete(base_sflow) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete(base_path + ['sflow']) + self.cli_commit() + + # Check interfaces are deleted from VPP sFlow + _, out = rc_cmd('sudo vppctl show sflow') + self.assertIn('interfaces enabled: 0', out) + + def test_16_resource_limits(self): + max_map_count = '100000' + shmmax = '55555555555555' + hr_path = ['system', 'option', 'resource-limits'] + + # Check if max-map-count has default auto calculated value + # but not less than '65530' + self.assertEqual(sysctl_read(['vm', 'max_map_count']), '65530') + # The same is with: kernel.shmmax = '8589934592' + self.assertEqual(sysctl_read(['kernel', 'shmmax']), '8589934592') + + # Change max-map-count, shmmax and check + self.cli_set(hr_path + ['max-map-count', max_map_count]) + self.cli_set(hr_path + ['shmmax', shmmax]) + self.cli_commit() + + self.assertEqual(sysctl_read(['vm', 'max_map_count']), max_map_count) + self.assertEqual(sysctl_read(['kernel', 'shmmax']), shmmax) + + # We expect max-map-count and shmmax will return auto calculated values + self.cli_delete(hr_path + ['max-map-count']) + self.cli_delete(hr_path + ['shmmax']) + self.cli_commit() + + self.assertEqual(sysctl_read(['vm', 'max_map_count']), '65530') + self.assertEqual(sysctl_read(['kernel', 'shmmax']), '8589934592') + + def test_17_1_vpp_pppoe_mapping(self): + config_file = '/run/accel-pppd/pppoe.conf' + pool = "TEST-POOL" + vni = '23' + pppoe_base = ['service', 'pppoe-server'] + + self.cli_set(['interfaces', 'ethernet', interface, 'vif', vni]) + + # Basic pppoe-server config + self.cli_set(pppoe_base + ['authentication', 'mode', 'noauth']) + self.cli_set(pppoe_base + ['gateway-address', '192.0.2.1']) + self.cli_set(pppoe_base + ['client-ip-pool', pool, 'range', '192.0.2.0/24']) + self.cli_set(pppoe_base + ['default-pool', pool]) + + self.cli_set(pppoe_base + ['interface', interface]) + self.cli_set(pppoe_base + ['interface', f'{interface}.{vni}']) + + self.cli_commit() + + # Validate configuration values + config = read_file(config_file) + + # Validate configuration + # PPPoE on VPP-managed interfaces automatically get control-plane integration + self.assertIn(f'interface={interface},vpp-cp=true', config) + self.assertIn(f'interface={interface}.{vni},vpp-cp=true', config) + + # Check pppoe mapping + _, out = rc_cmd('sudo vppctl show pppoe control-plane binding') + self.assertRegex(out, rf'{interface}\s+tap4096') + self.assertRegex(out, rf'{interface}.{vni}\s+tap4096.23') + + # check if dependency is called and mapping is correct after changes in vpp script + self.cli_set( + base_path + ['settings', 'interface', interface, 'num-tx-desc', '512'] + ) + self.cli_commit() + + # Check pppoe mapping + _, out = rc_cmd('sudo vppctl show pppoe control-plane binding') + self.assertRegex(out, rf'{interface}\s+tap4096') + self.assertRegex(out, rf'{interface}.{vni}\s+tap4096.23') + + # delete PPPoE config + self.cli_delete(pppoe_base) + + # delete vif Ethernet interface + self.cli_delete(['interfaces', 'ethernet', interface, 'vif']) + self.cli_commit() + + def test_17_2_vpp_pppoe_invalid_vif(self): + # Test verify step behavior when referenced PPPoE interface does not actually exist + pool = "TEST-POOL-2" + vni = '24' + pppoe_base = ['service', 'pppoe-server'] + + # Basic pppoe-server config + self.cli_set(pppoe_base + ['authentication', 'mode', 'noauth']) + self.cli_set(pppoe_base + ['gateway-address', '192.0.3.1']) + self.cli_set(pppoe_base + ['client-ip-pool', pool, 'range', '192.0.3.0/24']) + self.cli_set(pppoe_base + ['default-pool', pool]) + + self.cli_set(pppoe_base + ['interface', interface, 'combined']) + self.cli_set(pppoe_base + ['interface', f'{interface}.{vni}']) + + err_msg = f'Virtual Interface "{interface}.{vni}" does not exist' + with self.assertRaisesRegex(ConfigSessionError, err_msg): + self.cli_commit() + + # The second commit can throw exception instead of verify error: + # - `FileNotFoundError: PCI device tap does not exist` + # More details here: https://vyos.dev/T8276 + with self.assertRaisesRegex(ConfigSessionError, err_msg): + self.cli_commit() + self.assertTrue(interface_exists(interface)) + + self.cli_set(['interfaces', 'ethernet', interface, 'vif', vni]) + self.cli_commit() + + # Cleanup PPPoE server configuration and created VIF + self.cli_delete(pppoe_base) + self.cli_delete(['interfaces', 'ethernet', interface, 'vif', vni]) + self.cli_commit() + + def test_17_3_vpp_pppoe_delete_invalid_vif(self): + # Test verify step behavior when referenced PPPoE virtual interface was deleted + pool = "TEST-POOL-3" + vni = '25' + pppoe_base = ['service', 'pppoe-server'] + + # Basic pppoe-server config + self.cli_set(pppoe_base + ['authentication', 'mode', 'noauth']) + self.cli_set(pppoe_base + ['gateway-address', '192.0.4.1']) + self.cli_set(pppoe_base + ['client-ip-pool', pool, 'range', '192.0.4.0/24']) + self.cli_set(pppoe_base + ['default-pool', pool]) + self.cli_set(pppoe_base + ['interface', interface, 'combined']) + self.cli_set(pppoe_base + ['interface', f'{interface}.{vni}']) + + err_msg = f'Virtual Interface "{interface}.{vni}" does not exist' + with self.assertRaisesRegex(ConfigSessionError, err_msg): + self.cli_commit() + + self.cli_delete(pppoe_base + ['interface', f'{interface}.{vni}']) + self.cli_commit() + + # Cleanup PPPoE server configuration and created VIF + self.cli_delete(pppoe_base) + self.cli_commit() + + def test_17_4_vpp_pppoe_invalid_sub_vif(self): + # Test verify step behavior when referenced PPPoE + # sub-interface which have several tags does not exist + pool = "TEST-POOL-4" + vif_s, vif_c = '26', '10' + pppoe_base = ['service', 'pppoe-server'] + + # Basic pppoe-server config + self.cli_set(pppoe_base + ['authentication', 'mode', 'noauth']) + self.cli_set(pppoe_base + ['gateway-address', '192.0.5.1']) + self.cli_set(pppoe_base + ['client-ip-pool', pool, 'range', '192.0.5.0/24']) + self.cli_set(pppoe_base + ['default-pool', pool]) + + self.cli_set(pppoe_base + ['interface', interface, 'combined']) + self.cli_set(pppoe_base + ['interface', f'{interface}.{vif_s}.{vif_c}']) + + err_msg = f'Virtual Interface "{interface}.{vif_s}.{vif_c}" does not exist' + with self.assertRaisesRegex(ConfigSessionError, err_msg): + self.cli_commit() + + # The second commit can throw exception instead of verify error: + # - `FileNotFoundError: PCI device tap does not exist` + # More details here: https://vyos.dev/T8276 + with self.assertRaisesRegex(ConfigSessionError, err_msg): + self.cli_commit() + self.assertTrue(interface_exists(interface)) + + self.cli_set( + ['interfaces', 'ethernet', interface, 'vif-s', vif_s, 'vif-c', vif_c] + ) + self.cli_commit() + + # Cleanup PPPoE server configuration and created VIF + self.cli_delete(pppoe_base) + self.cli_delete(['interfaces', 'ethernet', interface, 'vif-s', vif_s]) + self.cli_commit() + + def test_18_1_kernel_options_hugepages(self): + default_hp_size = '2M' + hp_size_1g = '1G' + hp_size_2m = '2M' + hp_count_1g = '2' + hp_count_2m = '512' + memory_path = ['system', 'option', 'kernel', 'memory'] + + self.cli_set(memory_path + ['default-hugepage-size', default_hp_size]) + self.cli_set( + memory_path + ['hugepage-size', hp_size_2m, 'hugepage-count', hp_count_2m] + ) + self.cli_set( + memory_path + ['hugepage-size', hp_size_1g, 'hugepage-count', '2000'] + ) + # very big number of 1G hugepages, not enough memory for configuring them + # expect raise ConfigError + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + memory_path + ['hugepage-size', hp_size_1g, 'hugepage-count', hp_count_1g] + ) + 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(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) + + def test_18_2_kernel_options_cpu(self): + isolate_cpus = '1,2' + + self.cli_set( + ['system', 'option', 'kernel', 'cpu', 'isolate-cpus', isolate_cpus] + ) + 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(f' isolcpus={isolate_cpus}', tmp) + + # verify 'isolate-cpus' are set not correctly + # expect raise ConfigError + self.cli_set(['system', 'option', 'kernel', 'cpu', 'isolate-cpus', '1-99']) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_discard() + + def test_19_static_arp(self): + host = '192.0.2.10' + mac = '00:01:02:03:04:0a' + path_static_arp = ['protocols', 'static', 'arp'] + + self.cli_set(['interfaces', 'ethernet', interface, 'address', '192.0.2.1/24']) + self.cli_set( + path_static_arp + ['interface', interface, 'address', host, 'mac', mac] + ) + self.cli_commit() + + # Change VPP configuration + self.cli_set(base_path + ['settings', 'poll-sleep-usec', '50']) + + # Ensure arp entry is not disappeared + _, neighbors = rc_cmd('sudo ip neighbor') + self.assertIn(f'{host} dev {interface} lladdr {mac}', neighbors) + + # Check VPP IP neighbors + _, vpp_neighbors = rc_cmd('sudo vppctl show ip neighbors') + self.assertRegex(vpp_neighbors, rf'{host}\s+S\s+{mac}\s+{interface}') + + self.cli_delete(path_static_arp) + + def test_20_1_vpp_ipfix(self): + base_ipfix = base_path + ['ipfix'] + base_collector = base_ipfix + ['collector'] + collector_ip = '127.0.0.2' + collector_src = '127.0.0.1' + collector_port = '9374' + timer_active = '8' + timer_passive = '32' + tmplt_interval = '4' + flow_probe_rec = 'l3' + not_vpp_interface = 'eth0' + + self.cli_set(base_ipfix + ['active-timeout', timer_active]) + self.cli_set(base_ipfix + ['inactive-timeout', timer_passive]) + self.cli_set(base_ipfix + ['flowprobe-record', flow_probe_rec]) + self.cli_set(base_ipfix + ['interface', interface]) + self.cli_set(base_collector + [collector_ip, 'source-address', collector_src]) + self.cli_set(base_collector + [collector_ip, 'port', collector_port]) + self.cli_set( + base_collector + [collector_ip, 'template-interval', tmplt_interval] + ) + self.cli_commit() + + # Test 1: Verify flowprobe parameters + _, out = rc_cmd('sudo vppctl show flowprobe params') + required_str = ( + f'{flow_probe_rec} active: {timer_active} passive: {timer_passive}' + ) + self.assertIn(required_str, out) + + # Test 2: Add non-VPP interface + self.cli_set(base_ipfix + ['interface', not_vpp_interface]) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete(base_ipfix + ['interface', not_vpp_interface]) + self.cli_set(base_ipfix + ['interface', interface]) + self.cli_commit() + + _, out = rc_cmd('sudo vppctl show flowprobe feature') + required_str = f'{interface} ip4 rx tx' + self.assertIn(required_str, out) + + # Test 3: Verify IPFIX exporter via API + # Set socket permissions to allow test access (owner/group read/write only) + if os.path.exists('/run/vpp/api.sock'): + os.system('sudo chmod 666 /run/vpp/api.sock') + + vpp = VPPControl() + + # Get all exporters + result = vpp.api.ipfix_all_exporter_get() + # Second element contains the exporter list + exporters = result[1] + + # Find our configured exporter + found_exporter = None + for exporter in exporters: + if str(exporter.collector_address) == collector_ip: + found_exporter = exporter + break + + # Verify exporter parameters + self.assertIsNotNone(found_exporter, 'IPFIX exporter not found') + self.assertEqual(str(found_exporter.collector_address), collector_ip) + self.assertEqual(str(found_exporter.src_address), collector_src) + self.assertEqual(found_exporter.collector_port, int(collector_port)) + self.assertEqual(found_exporter.template_interval, int(tmplt_interval)) + self.assertEqual(found_exporter.path_mtu, 512) # Default path MTU + self.assertEqual(found_exporter.vrf_id, 0) # Default VRF + self.assertFalse(found_exporter.udp_checksum) # Default UDP checksum + + # Test 4: Cleanup - remove configuration + self.cli_delete(base_ipfix) + self.cli_commit() + + # Verify cleanup + result = vpp.api.ipfix_all_exporter_get() + exporters = result[1] + # Should only have default exporter (0.0.0.0) left + non_default_exporters = [ + e for e in exporters if str(e.collector_address) != '0.0.0.0' + ] + self.assertEqual( + len(non_default_exporters), 0, 'Exporters not cleaned up properly' + ) + + def test_20_2_vpp_ipfix_bond(self): + base_ipfix = base_path + ['ipfix'] + base_bond = interfaces_path + ['bonding'] + iface_bond = 'vppbond0' + collector_ip = '127.0.0.2' + collector_src = '127.0.0.1' + + self.cli_set(base_bond + [iface_bond, 'member', 'interface', interface]) + + self.cli_set( + base_ipfix + ['collector', collector_ip, 'source-address', collector_src] + ) + self.cli_set(base_ipfix + ['interface', iface_bond]) + self.cli_commit() + + vpp_bond_name = vpp_iface_name_transform(iface_bond) + required_str = f'{vpp_bond_name} ip4 rx tx' + + # Check bonding interface is added to IPFIX + _, out = rc_cmd('sudo vppctl show flowprobe feature') + self.assertIn(required_str, out) + + # Change bonding interface configuration + self.cli_set(base_bond + [iface_bond, 'mode', '802.3ad']) + self.cli_commit() + + # Check interface + _, out = rc_cmd('sudo vppctl show flowprobe feature') + self.assertIn(required_str, out) + + def test_21_double_enabling_vpp(self): + # Verify double enabling of VPP + + # Delete already defined settings from 'setUp' method + self.cli_delete(base_path) + + # First commit changes + self.cli_set(base_path + ['settings', 'interface', interface]) + self.cli_set(base_path + ['settings', 'poll-sleep-usec', '20']) + self.cli_commit() + + # Delete all VPP changes + self.cli_delete(base_path) + self.cli_commit() + + # Second commit changes + self.cli_set(base_path + ['settings', 'interface', interface]) + self.cli_set(base_path + ['settings', 'poll-sleep-usec', '30']) + self.cli_commit() + + # Ensure that VPP process is active + self.assertTrue(process_named_running(PROCESS_NAME)) + + def test_22_no_vpp_kernel_bridge_cross_membership(self): + vlan = '123' + member = f'{interface}.{vlan}' + bridge_iface = 'br1' + + self.cli_commit() + + # Ensure that VPP process is active + self.assertTrue(process_named_running(PROCESS_NAME)) + + # Attempt to add a VPP interface VLAN as a bridge member + self.cli_set(['interfaces', 'ethernet', interface, 'vif', vlan]) + self.cli_set( + ['interfaces', 'bridge', bridge_iface, 'member', 'interface', member] + ) + + # Adding a VPP interface (or its VLAN) as a bridge member is not allowed + # expect raise ConfigError + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete(base_path) + self.cli_commit() + + # Ensure interface is a member of bridge + self.assertTrue(os.path.isdir(f'/sys/class/net/{bridge_iface}/lower_{member}')) + + # Adding a bridge member as a VPP interface is not allowed + # expect raise ConfigError + self.cli_set(base_path + ['settings', 'interface', interface]) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete(['interfaces', 'bridge']) + self.cli_commit() + + # Ensure that VPP process is active + self.assertTrue(process_named_running(PROCESS_NAME)) + + def test_23_vpp_acl_subinterface(self): + base_acl = base_path + ['acl', 'ip'] + vlan = '200' + subif = f'{interface}.{vlan}' + acl_name = 'STATEFUL' + acl_tag = '10' + rule = '10' + + self.cli_set(['interfaces', 'ethernet', interface, 'vif', vlan]) + self.cli_set( + base_acl + ['tag-name', acl_name, 'rule', rule, 'action', 'permit'] + ) + self.cli_set( + base_acl + + ['interface', subif, 'input', 'acl-tag', acl_tag, 'tag-name', acl_name] + ) + self.cli_commit() + + vpp = VPPControl() + subif_index = vpp.get_sw_if_index(subif) + self.assertIsNotNone(subif_index) + + acl_index = None + for acl in vpp.api.acl_dump(acl_index=0xFFFFFFFF): + if acl.tag == acl_name: + acl_index = acl.acl_index + break + self.assertIsNotNone(acl_index) + + acl_interfaces = [ + entry + for entry in vpp.api.acl_interface_list_dump() + if entry.sw_if_index == subif_index and entry.count != 0 + ] + self.assertEqual(len(acl_interfaces), 1) + self.assertEqual(acl_interfaces[0].n_input, 1) + self.assertEqual( + list(acl_interfaces[0].acls)[: acl_interfaces[0].count], [acl_index] + ) + + +if __name__ == '__main__': + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/smoketest/scripts/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py index 30980f9ec..902308b9b 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-2025 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -22,23 +22,26 @@ 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 from vyos.utils.file import read_file +from vyos.utils.misc import wait_for from vyos.utils.network import get_interface_config from vyos.utils.network import get_vrf_tableid from vyos.utils.network import is_intf_addr_assigned from vyos.utils.network import interface_exists from vyos.utils.process import cmd from vyos.utils.system import sysctl_read +from vyos.template import inc_ip +from vyos.utils.process import process_named_running +from vyos.xml_ref import default_value base_path = ['vrf'] vrfs = ['red', 'green', 'blue', 'foo-bar', 'baz_foo'] -v4_protocols = ['any', 'babel', 'bgp', 'connected', 'eigrp', 'isis', 'kernel', 'ospf', 'rip', 'static', 'table'] -v6_protocols = ['any', 'babel', 'bgp', 'connected', 'isis', 'kernel', 'ospfv3', 'ripng', 'static', 'table'] +v4_protocols = ['any', 'babel', 'bgp', 'eigrp', 'isis', 'ospf', 'rip', 'static'] +v6_protocols = ['any', 'babel', 'bgp', 'isis', 'ospfv3', 'ripng', 'static'] class VRFTest(VyOSUnitTestSHIM.TestCase): _interfaces = [] @@ -54,14 +57,14 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): 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() def setUp(self): - # VRF strict_most ist always enabled + # always forward to base class + super().setUp() + + # VRF strict_most is always enabled tmp = read_file('/proc/sys/net/vrf/strict_mode') self.assertEqual(tmp, '1') @@ -71,6 +74,44 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): self.cli_commit() for vrf in vrfs: self.assertFalse(interface_exists(vrf)) + # always forward to base class + super().tearDown() + + def walk_path(self, obj, path): + current = obj + + for i, key in enumerate(path): + if isinstance(key, str): + self.assertTrue(isinstance(current, dict), msg=f'Failed path: {path}') + self.assertTrue(key in current, msg=f'Failed path: {path}') + elif isinstance(key, int): + 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' + + current = current[key] + + return current + + def verify_config_object(self, obj, path, value): + base_obj = self.walk_path(obj, path) + self.assertTrue(isinstance(base_obj, list)) + self.assertTrue(any(True for v in base_obj if v == value)) + + def verify_config_value(self, obj, path, key, value): + base_obj = self.walk_path(obj, path) + if isinstance(base_obj, list): + self.assertTrue(any(True for v in base_obj if key in v and v[key] == value)) + elif isinstance(base_obj, dict): + self.assertTrue(key in base_obj) + self.assertEqual(base_obj[key], value) + + def verify_kea_service_running(self, process_name): + tmp = cmd('tail -n 100 /var/log/messages') + self.assertTrue( + process_named_running(process_name), msg=f'Service not running, log: {tmp}' + ) def test_vrf_vni_and_table_id(self): base_table = '1000' @@ -118,7 +159,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}', endsection='^exit-vrf') + frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) self.assertEqual(int(table), get_vrf_tableid(vrf)) @@ -142,8 +183,8 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Ensure VRF was created self.assertTrue(interface_exists(vrf)) # Verify IP forwarding is 1 (enabled) - self.assertEqual(sysctl_read(f'net.ipv4.conf.{vrf}.forwarding'), '1') - self.assertEqual(sysctl_read(f'net.ipv6.conf.{vrf}.forwarding'), '1') + self.assertEqual(sysctl_read(['net', 'ipv4', 'conf', vrf, 'forwarding']), '1') + self.assertEqual(sysctl_read(['net', 'ipv6', 'conf', vrf, 'forwarding']), '1') # Test for proper loopback IP assignment for addr in loopbacks: @@ -162,11 +203,11 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify VRF configuration - self.assertEqual(sysctl_read('net.ipv4.tcp_l3mdev_accept'), '1') - self.assertEqual(sysctl_read('net.ipv4.udp_l3mdev_accept'), '1') + self.assertEqual(sysctl_read(['net', 'ipv4', 'tcp_l3mdev_accept']), '1') + self.assertEqual(sysctl_read(['net', 'ipv4', 'udp_l3mdev_accept']), '1') # If there is any VRF defined, strict_mode should be on - self.assertEqual(sysctl_read('net.vrf.strict_mode'), '1') + self.assertEqual(sysctl_read(['net', 'vrf', 'strict_mode']), '1') def test_vrf_table_id_is_unalterable(self): # Linux Kernel prohibits the change of a VRF table on the fly. @@ -239,7 +280,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): self.assertTrue(interface_exists(vrf)) - frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') + frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) self.assertIn(f' ip route {prefix} {next_hop}', frrconfig) @@ -285,6 +326,48 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): self.cli_delete(['interfaces', 'dummy', interface]) self.cli_commit() + def test_delete_vrf_protocols_should_not_crash(self): + # Testcase for issue T7255: + # - verify that deleting the 'protocols' node under a VRF does not crash. + + table = '3000' + vrf = 'purple' + interface = 'dum3000' + router_id = '10.2.0.2' + + # Configure dummy interface and assign to VRF + self.cli_set(['interfaces', 'dummy', interface, 'address', '10.1.0.254/24']) + self.cli_set(['interfaces', 'dummy', interface, 'vrf', vrf]) + + # Configure OSPF under the VRF + base_ospf_path = base_path + ['name', vrf, 'protocols', 'ospf'] + self.cli_set(base_ospf_path + ['interface', interface, 'area', '0']) + self.cli_set(base_ospf_path + ['parameters', 'router-id', router_id]) + self.cli_set(['protocols', 'ospf']) + + # Assign routing table number to the VRF + self.cli_set(base_path + ['name', vrf, 'table', table]) + + # Commit configuration and verify VRF was successfully created + self.cli_commit() + self.assertTrue(interface_exists(vrf)) + frrconfig = self.getFRRconfig(f'router ospf vrf {vrf}', stop_section='^exit') + self.assertIn(f'ospf router-id {router_id}', frrconfig) + + try: + # Attempt to delete the entire 'protocols' subtree under VRF + self.cli_delete(base_path + ['name', vrf, 'protocols']) + self.cli_commit() + + # Verify result of deleting 'protocols' subtree + frrconfig = self.getFRRconfig(f'router ospf vrf {vrf}', stop_section='^exit') + self.assertNotIn(f'ospf router-id {router_id}', frrconfig) + finally: + # Clean up dummy interface and VRF and re-commit + self.cli_delete(['interfaces', 'dummy', interface]) + self.cli_delete(base_path + ['name', vrf]) + self.cli_commit() + def test_vrf_disable_forwarding(self): table = '2000' for vrf in vrfs: @@ -303,8 +386,8 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Ensure VRF was created self.assertTrue(interface_exists(vrf)) # Verify IP forwarding is 0 (disabled) - self.assertEqual(sysctl_read(f'net.ipv4.conf.{vrf}.forwarding'), '0') - self.assertEqual(sysctl_read(f'net.ipv6.conf.{vrf}.forwarding'), '0') + self.assertEqual(sysctl_read(['net', 'ipv4', 'conf', vrf, 'forwarding']), '0') + self.assertEqual(sysctl_read(['net', 'ipv6', 'conf', vrf, 'forwarding']), '0') def test_vrf_ip_protocol_route_map(self): table = '6000' @@ -323,7 +406,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Verify route-map properly applied to FRR for vrf in vrfs: - frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') + frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^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) @@ -338,7 +421,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Verify route-map properly is removed from FRR for vrf in vrfs: - frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') + frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf') self.assertNotIn(f' ip protocol', frrconfig) def test_vrf_ip_ipv6_protocol_non_existing_route_map(self): @@ -386,7 +469,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Verify route-map properly applied to FRR for vrf in vrfs: - frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') + frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf') self.assertIn(f'vrf {vrf}', frrconfig) for protocol in v6_protocols: # VyOS and FRR use a different name for OSPFv3 (IPv6) @@ -405,7 +488,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Verify route-map properly is removed from FRR for vrf in vrfs: - frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') + frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf') self.assertNotIn(f' ipv6 protocol', frrconfig) def test_vrf_vni_duplicates(self): @@ -435,7 +518,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): for vrf in vrfs: self.assertTrue(interface_exists(vrf)) - frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') + frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) # Increment table ID for the next run table = str(int(table) + 1) @@ -457,7 +540,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): for vrf in vrfs: self.assertTrue(interface_exists(vrf)) - frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') + frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) # Increment table ID for the next run table = str(int(table) + 1) @@ -480,7 +563,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): for vrf in vrfs: self.assertTrue(interface_exists(vrf)) - frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') + frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) # Increment table ID for the next run table = str(int(table) + 2) @@ -500,7 +583,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): for vrf in vrfs: self.assertTrue(interface_exists(vrf)) - frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') + frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) # Increment table ID for the next run table = str(int(table) + 2) @@ -508,7 +591,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}', endsection='^exit-vrf') + frrconfig = self.getFRRconfig(f'vrf {purple}', stop_section='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) # Now delete all the VNIs @@ -523,12 +606,12 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): for vrf in vrfs: self.assertTrue(interface_exists(vrf)) - frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') + frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf') self.assertNotIn('vni', frrconfig) # Verify purple VNI remains self.assertTrue(interface_exists(purple)) - frrconfig = self.getFRRconfig(f'vrf {purple}', endsection='^exit-vrf') + frrconfig = self.getFRRconfig(f'vrf {purple}', stop_section='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) def test_vrf_ip_ipv6_nht(self): @@ -546,7 +629,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Verify route-map properly applied to FRR for vrf in vrfs: - frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') + frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^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) @@ -561,7 +644,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Verify route-map properly is removed from FRR for vrf in vrfs: - frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') + frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf') self.assertNotIn(f' no ip nht resolve-via-default', frrconfig) self.assertNotIn(f' no ipv6 nht resolve-via-default', frrconfig) @@ -601,5 +684,464 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): self.cli_delete(['nat']) + def test_vrf_policy_based_route(self): + vrf_name = 'test-pbr_123' + + self.cli_set(base_path + ['name', vrf_name, 'table', '17563']) + + policy_path = ['policy', 'route', 'pbr_smoke4', 'rule', '10'] + self.cli_set(policy_path + ['action', 'accept']) + self.cli_set(policy_path + ['set', 'vrf', vrf_name]) + self.cli_set(policy_path + ['source', 'address', '192.0.2.1/32']) + + policy6_path = ['policy', 'route6', 'pbr_smoke6', 'rule', '10'] + self.cli_set(policy6_path + ['action', 'accept']) + self.cli_set(policy6_path + ['set', 'vrf', vrf_name]) + self.cli_set(policy6_path + ['source', 'address', '2001:db8::/56']) + + local_policy_path = ['policy', 'local-route', 'rule', '10'] + self.cli_set(local_policy_path + ['set', 'vrf', vrf_name]) + self.cli_set(local_policy_path + ['source', 'address', '192.0.2.1/32']) + + local_policy6_path = ['policy', 'local-route6', 'rule', '10'] + self.cli_set(local_policy6_path + ['set', 'vrf', vrf_name]) + self.cli_set(local_policy6_path + ['source', 'address', '2001:db8::/56']) + + self.cli_commit() + + self.cli_delete(base_path) + # check validate() - VRF referenced in policy based routing + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(['policy', 'route']) + # check validate() - VRF referenced in policy based routing + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(['policy', 'route6']) + # check validate() - VRF referenced in policy based routing + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(['policy', 'local-route']) + # check validate() - VRF referenced in policy based routing + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(['policy', 'local-route6']) + + self.cli_commit() + + def test_dhcp_single_pool(self): + # Prepare the vrf and options + table = '100' + vrf = 'dhcp_smoke' + interface = 'dum8888' + subnet = '192.0.2.0/25' + router = inc_ip(subnet, 1) + dns_1 = inc_ip(subnet, 2) + dns_2 = inc_ip(subnet, 3) + domain_name = 'vyos.net' + + # declare files + process_name = 'kea-dhcp4' + kea4_conf = f'/var/run/kea/kea-{vrf}-dhcp4.conf' + + # create interface + cidr_mask = subnet.split('/')[-1] + self.cli_set( + ['interfaces', 'dummy', interface, 'address', f'{router}/{cidr_mask}'] + ) + self.cli_set(['interfaces', 'dummy', interface, 'vrf', f'{vrf}']) + + # create the vrf with table + base = base_path + ['name', vrf] + self.cli_set(base + ['table', table]) + + # set the dhcp scope + base = base_path + ['name', vrf, 'service', 'dhcp-server'] + shared_net_name = 'SMOKE-1' + + range_0_start = inc_ip(subnet, 10) + range_0_stop = inc_ip(subnet, 20) + range_1_start = inc_ip(subnet, 40) + range_1_stop = inc_ip(subnet, 50) + + self.cli_set(base + ['listen-interface', interface]) + + self.cli_set(base + ['shared-network-name', shared_net_name, 'ping-check']) + + pool = base + ['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]) + self.cli_set(pool + ['option', 'name-server', dns_2]) + self.cli_set(pool + ['option', 'domain-name', domain_name]) + + # check validate() - No DHCP address range or active static-mapping set + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(pool + ['range', '0', 'start', range_0_start]) + self.cli_set(pool + ['range', '0', 'stop', range_0_stop]) + self.cli_set(pool + ['range', '1', 'start', range_1_start]) + self.cli_set(pool + ['range', '1', 'stop', range_1_stop]) + + # commit changes + self.cli_commit() + + 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 + ) + + # 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}, + ) + self.verify_config_object( + 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}, + ) + + # Verify pools + self.verify_config_object( + 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}'}, + ) + + # Check for running process + self.verify_kea_service_running(process_name) + + # perform cleanup + self.cli_delete(['interfaces', 'dummy', interface, 'address']) + self.cli_delete(['interfaces', 'dummy', interface, 'vrf']) + self.cli_delete(base) + self.cli_commit() + + def test_dhcp_vrf_default_route(self): + # T7927 - when retrieving a default route via DHCP, check that additional + # calls into FRRender() keep the DHCP route in place + vrf_name = 'red-16' + default_gateway = '192.0.2.1' + dhcp_if_server = 'veth0' + dhcp_if_client = 'veth1' + + default_distance = default_value(['interfaces', 'virtual-ethernet', + dhcp_if_client, 'dhcp-options', + 'default-route-distance']) + + dhcp_pool_base = ['service', 'dhcp-server', 'shared-network-name', + 'FOO-4', 'subnet', '192.0.2.0/24'] + veth_base = ['interfaces', 'virtual-ethernet'] + + # Start DHCP Server in VRF connected via veth pair to default VRF + self.cli_set(['vrf', 'name', vrf_name, 'table', '48752']) + self.cli_set(veth_base + [dhcp_if_server, 'address', '192.0.2.1/24']) + self.cli_set(veth_base + [dhcp_if_server, 'peer-name', dhcp_if_client]) + self.cli_set(veth_base + [dhcp_if_client, 'peer-name', dhcp_if_server]) + self.cli_set(veth_base + [dhcp_if_client, 'vrf', vrf_name]) + + self.cli_set(['service', 'dhcp-server', 'listen-interface', dhcp_if_server]) + self.cli_set(dhcp_pool_base + ['option', 'default-router', default_gateway]) + self.cli_set(dhcp_pool_base + ['range', 'uno', 'start', '192.0.2.10']) + self.cli_set(dhcp_pool_base + ['range', 'uno', 'stop', '192.0.2.30']) + self.cli_set(dhcp_pool_base + ['subnet-id', '1']) + + self.cli_commit() + + # Start DHCP client in VRF + self.cli_set(['interfaces', 'virtual-ethernet', dhcp_if_client, 'address', 'dhcp']) + self.cli_commit() + + # define helper for the string we are looking for in FRR configuration + # the leading whitespace is required as this lives under a VRF context! + test_ok_string = f' ip route 0.0.0.0/0 {default_gateway} {dhcp_if_client}'\ + f' tag 210 {default_distance}' + + def test_callback(self, vrf_name, string) -> bool: + tmp = self.getFRRconfig(f'^vrf {vrf_name}', stop_section='^exit-vrf') + return bool(string in tmp) + + # We need to wait until DHCP client has started and an IP address has been received + tmp = wait_for(test_callback, self, vrf_name, test_ok_string, timeout=20.0) + self.assertTrue(tmp) + + # Change anything in FRR to re-trigger config generation. DHCP route + # must still be present + self.cli_set(['protocols', 'static', 'route', '10.0.0.0/24', 'blackhole']) + self.cli_commit() + + frrconfig = self.getFRRconfig(f'^vrf {vrf_name}', stop_section='^exit-vrf') + self.assertIn(test_ok_string, frrconfig) + + self.cli_delete(['interfaces', 'virtual-ethernet']) + self.cli_delete(['service', 'dhcp-server']) + + def test_dhcpv6_single_pool(self): + # Prepare the vrf and other options + table = '100' + vrf = 'dhcp_smoke' + subnet = '2001:db8:f00::/64' + dns_1 = '2001:db8::1' + dns_2 = '2001:db8::2' + domain = 'vyos.net' + nis_servers = ['2001:db8:ffff::1', '2001:db8:ffff::2'] + interface = 'dum8888' + interface_addr = inc_ip(subnet, 1) + '/64' + + # declare files + process_name = 'kea-dhcp6' + kea6_conf = f'/var/run/kea/kea-{vrf}-dhcp6.conf' + + # create interface + self.cli_set(['interfaces', 'dummy', interface, 'address', f'{interface_addr}']) + self.cli_set(['interfaces', 'dummy', interface, 'vrf', f'{vrf}']) + + # create the vrf with table + base = base_path + ['name', vrf] + self.cli_set(base + ['table', table]) + + # set the dhcp scope + base = base_path + ['name', vrf, 'service', 'dhcpv6-server'] + + shared_net_name = 'SMOKE-1' + search_domains = ['foo.vyos.net', 'bar.vyos.net'] + lease_time = '1200' + max_lease_time = '72000' + min_lease_time = '600' + preference = '10' + sip_server = 'sip.vyos.net' + sntp_server = inc_ip(subnet, 100) + range_start = inc_ip(subnet, 256) # ::100 + range_stop = inc_ip(subnet, 65535) # ::ffff + + pool = base + ['shared-network-name', shared_net_name, 'subnet', subnet] + + self.cli_set(base + ['preference', preference]) + self.cli_set(pool + ['interface', interface]) + self.cli_set(pool + ['subnet-id', '1']) + # we use the first subnet IP address as default gateway + 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]) + self.cli_set(pool + ['option', 'nis-domain', domain]) + self.cli_set(pool + ['option', 'nisplus-domain', domain]) + self.cli_set(pool + ['option', 'sip-server', sip_server]) + self.cli_set(pool + ['option', 'sntp-server', sntp_server]) + self.cli_set(pool + ['range', '1', 'start', range_start]) + self.cli_set(pool + ['range', '1', 'stop', range_stop]) + + for server in nis_servers: + self.cli_set(pool + ['option', 'nis-server', server]) + self.cli_set(pool + ['option', 'nisplus-server', server]) + + for search_domain in search_domains: + self.cli_set(pool + ['option', 'domain-search', search_domain]) + + client_base = 1 + for client in ['client1', 'client2', 'client3']: + duid = f'00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:{client_base:02}' + self.cli_set(pool + ['static-mapping', client, 'duid', duid]) + self.cli_set( + pool + + [ + 'static-mapping', + client, + 'ipv6-address', + inc_ip(subnet, client_base), + ] + ) + self.cli_set( + pool + + [ + 'static-mapping', + client, + 'ipv6-prefix', + inc_ip(subnet, client_base << 64) + '/64', + ] + ) + client_base += 1 + + # cannot have both mac-address and duid set + with self.assertRaises(ConfigSessionError): + self.cli_set( + pool + ['static-mapping', 'client1', 'mac', '00:50:00:00:00:11'] + ) + self.cli_commit() + self.cli_delete(pool + ['static-mapping', 'client1', 'mac']) + + # commit changes + self.cli_commit() + + config = read_file(kea6_conf) + obj = loads(config) + + self.verify_config_value( + obj, ['Dhcp6', 'shared-networks'], 'name', shared_net_name + ) + self.verify_config_value( + obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'subnet', subnet + ) + self.verify_config_value( + obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'interface', interface + ) + self.verify_config_value( + obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'id', 1 + ) + self.verify_config_value( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6'], + 'valid-lifetime', + int(lease_time), + ) + self.verify_config_value( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6'], + 'min-valid-lifetime', + int(min_lease_time), + ) + self.verify_config_value( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6'], + 'max-valid-lifetime', + int(max_lease_time), + ) + + # Verify options + 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, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], + {'name': 'domain-search', 'data': ', '.join(search_domains)}, + ) + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], + {'name': 'nis-domain-name', 'data': domain}, + ) + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], + {'name': 'nis-servers', 'data': ', '.join(nis_servers)}, + ) + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], + {'name': 'nisp-domain-name', 'data': domain}, + ) + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], + {'name': 'nisp-servers', 'data': ', '.join(nis_servers)}, + ) + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], + {'name': 'sntp-servers', 'data': sntp_server}, + ) + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], + {'name': 'sip-server-dns', 'data': sip_server}, + ) + + # Verify pools + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'pools'], + {'pool': f'{range_start} - {range_stop}'}, + ) + + client_base = 1 + for client in ['client1', 'client2', 'client3']: + duid = f'00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:{client_base:02}' + ip = inc_ip(subnet, client_base) + prefix = inc_ip(subnet, client_base << 64) + '/64' + + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'reservations'], + { + 'hostname': client, + 'duid': duid, + 'ip-addresses': [ip], + 'prefixes': [prefix], + }, + ) + + client_base += 1 + + # Check for running process + self.verify_kea_service_running(process_name) + + # perform cleanup + self.cli_delete(['interfaces', 'dummy', interface, 'address']) + self.cli_delete(['interfaces', 'dummy', interface, 'vrf']) + self.cli_delete(base) + self.cli_commit() + + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) |
