summaryrefslogtreecommitdiff
path: root/smoketest/scripts/cli
diff options
context:
space:
mode:
Diffstat (limited to 'smoketest/scripts/cli')
-rw-r--r--smoketest/scripts/cli/base_accel_ppp_test.py15
-rw-r--r--smoketest/scripts/cli/base_interfaces_test.py134
-rw-r--r--smoketest/scripts/cli/base_vyostest_shim.py352
-rwxr-xr-xsmoketest/scripts/cli/test_backslash_escape.py4
-rwxr-xr-xsmoketest/scripts/cli/test_cgnat.py9
-rwxr-xr-xsmoketest/scripts/cli/test_config_dependency.py6
-rwxr-xr-xsmoketest/scripts/cli/test_config_save.py99
-rwxr-xr-xsmoketest/scripts/cli/test_configd_init.py26
-rwxr-xr-xsmoketest/scripts/cli/test_container.py293
-rwxr-xr-xsmoketest/scripts/cli/test_firewall.py346
-rwxr-xr-xsmoketest/scripts/cli/test_high-availability_virtual-server.py13
-rwxr-xr-xsmoketest/scripts/cli/test_high-availability_vrrp.py29
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_bonding.py81
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_bridge.py37
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_dummy.py6
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_ethernet.py111
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_geneve.py5
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_input.py9
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_l2tpv3.py7
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_loopback.py15
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_macsec.py7
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_openvpn.py52
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_pppoe.py283
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_pseudo-ethernet.py8
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_tunnel.py7
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_virtual-ethernet.py41
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_vti.py5
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_vxlan.py23
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_wireguard.py90
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_wireless.py30
-rwxr-xr-xsmoketest/scripts/cli/test_load-balancing_haproxy.py264
-rwxr-xr-xsmoketest/scripts/cli/test_load-balancing_wan.py209
-rwxr-xr-xsmoketest/scripts/cli/test_nat.py9
-rwxr-xr-xsmoketest/scripts/cli/test_nat64.py37
-rwxr-xr-xsmoketest/scripts/cli/test_nat66.py77
-rwxr-xr-xsmoketest/scripts/cli/test_netns.py7
-rwxr-xr-xsmoketest/scripts/cli/test_op-mode_show.py4
-rwxr-xr-xsmoketest/scripts/cli/test_pki.py6
-rwxr-xr-xsmoketest/scripts/cli/test_policy.py141
-rw-r--r--smoketest/scripts/cli/test_policy_local-route.py9
-rwxr-xr-xsmoketest/scripts/cli/test_policy_route.py43
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_babel.py19
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_bfd.py25
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_bgp.py338
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_failover.py649
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_igmp-proxy.py9
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_isis.py174
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_mpls.py43
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_nhrp.py8
-rw-r--r--smoketest/scripts/cli/test_protocols_openfabric.py47
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_ospf.py160
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_ospfv3.py43
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_pim.py17
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_pim6.py23
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_rip.py17
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_ripng.py13
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_rpki.py196
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_segment-routing.py187
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_static.py191
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_static_arp.py6
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_traffic-engineering.py123
-rwxr-xr-xsmoketest/scripts/cli/test_qos.py37
-rwxr-xr-xsmoketest/scripts/cli/test_service_broadcast-relay.py31
-rw-r--r--smoketest/scripts/cli/test_service_config-sync.py176
-rwxr-xr-xsmoketest/scripts/cli/test_service_dhcp-relay.py7
-rwxr-xr-xsmoketest/scripts/cli/test_service_dhcp-server.py649
-rwxr-xr-xsmoketest/scripts/cli/test_service_dhcpv6-relay.py4
-rwxr-xr-xsmoketest/scripts/cli/test_service_dhcpv6-server.py265
-rwxr-xr-xsmoketest/scripts/cli/test_service_dns_dynamic.py12
-rwxr-xr-xsmoketest/scripts/cli/test_service_dns_forwarding.py22
-rwxr-xr-xsmoketest/scripts/cli/test_service_https.py231
-rwxr-xr-xsmoketest/scripts/cli/test_service_ids_ddos-protection.py116
-rwxr-xr-xsmoketest/scripts/cli/test_service_ipoe-server.py27
-rwxr-xr-xsmoketest/scripts/cli/test_service_lldp.py8
-rwxr-xr-xsmoketest/scripts/cli/test_service_mdns_repeater.py19
-rw-r--r--smoketest/scripts/cli/test_service_monitoring_network_event.py6
-rwxr-xr-xsmoketest/scripts/cli/test_service_monitoring_prometheus.py6
-rwxr-xr-xsmoketest/scripts/cli/test_service_monitoring_telegraf.py8
-rwxr-xr-xsmoketest/scripts/cli/test_service_monitoring_zabbix-agent.py8
-rwxr-xr-xsmoketest/scripts/cli/test_service_ndp-proxy.py66
-rwxr-xr-xsmoketest/scripts/cli/test_service_ntp.py52
-rwxr-xr-xsmoketest/scripts/cli/test_service_pppoe-server.py6
-rwxr-xr-xsmoketest/scripts/cli/test_service_router-advert.py145
-rwxr-xr-xsmoketest/scripts/cli/test_service_salt-minion.py11
-rwxr-xr-xsmoketest/scripts/cli/test_service_snmp.py163
-rwxr-xr-xsmoketest/scripts/cli/test_service_ssh.py231
-rwxr-xr-xsmoketest/scripts/cli/test_service_stunnel.py6
-rwxr-xr-xsmoketest/scripts/cli/test_service_tftp-server.py10
-rwxr-xr-xsmoketest/scripts/cli/test_service_webproxy.py6
-rwxr-xr-xsmoketest/scripts/cli/test_system_acceleration_qat.py6
-rwxr-xr-xsmoketest/scripts/cli/test_system_conntrack.py239
-rwxr-xr-xsmoketest/scripts/cli/test_system_console.py88
-rwxr-xr-xsmoketest/scripts/cli/test_system_flow-accounting.py306
-rwxr-xr-xsmoketest/scripts/cli/test_system_frr.py66
-rwxr-xr-xsmoketest/scripts/cli/test_system_ip.py68
-rwxr-xr-xsmoketest/scripts/cli/test_system_ipv6.py41
-rwxr-xr-xsmoketest/scripts/cli/test_system_lcd.py4
-rwxr-xr-xsmoketest/scripts/cli/test_system_login.py54
-rwxr-xr-xsmoketest/scripts/cli/test_system_logs.py19
-rwxr-xr-xsmoketest/scripts/cli/test_system_option.py58
-rwxr-xr-xsmoketest/scripts/cli/test_system_resolvconf.py6
-rwxr-xr-xsmoketest/scripts/cli/test_system_sflow.py8
-rwxr-xr-xsmoketest/scripts/cli/test_system_syslog.py362
-rw-r--r--smoketest/scripts/cli/test_system_watchdog.py141
-rw-r--r--smoketest/scripts/cli/test_techsupport_archive.py251
-rw-r--r--smoketest/scripts/cli/test_techsupport_report.py94
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_ipsec.py605
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_l2tp.py28
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_openconnect.py9
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_pptp.py5
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_sstp.py7
-rwxr-xr-xsmoketest/scripts/cli/test_vpp.py1518
-rwxr-xr-xsmoketest/scripts/cli/test_vrf.py604
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())