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.py21
-rw-r--r--smoketest/scripts/cli/base_interfaces_test.py193
-rw-r--r--smoketest/scripts/cli/base_vyostest_shim.py355
-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.py355
-rwxr-xr-xsmoketest/scripts/cli/test_firewall.py496
-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.py94
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_bridge.py86
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_dummy.py6
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_ethernet.py154
-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.py32
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_wireguard.py194
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_wireless.py30
-rwxr-xr-xsmoketest/scripts/cli/test_load-balancing_haproxy.py (renamed from smoketest/scripts/cli/test_load-balancing_reverse-proxy.py)321
-rwxr-xr-xsmoketest/scripts/cli/test_load-balancing_wan.py353
-rwxr-xr-xsmoketest/scripts/cli/test_nat.py37
-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.py162
-rw-r--r--smoketest/scripts/cli/test_policy_local-route.py173
-rwxr-xr-xsmoketest/scripts/cli/test_policy_route.py55
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_babel.py221
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_bfd.py26
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_bgp.py553
-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.py210
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_mpls.py89
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_nhrp.py85
-rw-r--r--smoketest/scripts/cli/test_protocols_openfabric.py52
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_ospf.py179
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_ospfv3.py47
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_pim.py58
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_pim6.py37
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_rip.py23
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_ripng.py17
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_rpki.py193
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_segment-routing.py269
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_static.py336
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_static_arp.py6
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_static_multicast.py49
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_traffic-engineering.py123
-rwxr-xr-xsmoketest/scripts/cli/test_qos.py500
-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.py1546
-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.py53
-rwxr-xr-xsmoketest/scripts/cli/test_service_dns_forwarding.py22
-rwxr-xr-xsmoketest/scripts/cli/test_service_https.py316
-rwxr-xr-xsmoketest/scripts/cli/test_service_ids_ddos-protection.py116
-rwxr-xr-xsmoketest/scripts/cli/test_service_ipoe-server.py84
-rwxr-xr-xsmoketest/scripts/cli/test_service_lldp.py55
-rwxr-xr-xsmoketest/scripts/cli/test_service_mdns_repeater.py95
-rw-r--r--smoketest/scripts/cli/test_service_monitoring_network_event.py67
-rwxr-xr-xsmoketest/scripts/cli/test_service_monitoring_prometheus.py163
-rwxr-xr-xsmoketest/scripts/cli/test_service_monitoring_telegraf.py8
-rwxr-xr-xsmoketest/scripts/cli/test_service_monitoring_zabbix-agent.py29
-rwxr-xr-xsmoketest/scripts/cli/test_service_ndp-proxy.py66
-rwxr-xr-xsmoketest/scripts/cli/test_service_ntp.py135
-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.py250
-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.py22
-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.py399
-rwxr-xr-xsmoketest/scripts/cli/test_system_frr.py66
-rwxr-xr-xsmoketest/scripts/cli/test_system_ip.py96
-rwxr-xr-xsmoketest/scripts/cli/test_system_ipv6.py69
-rwxr-xr-xsmoketest/scripts/cli/test_system_lcd.py4
-rwxr-xr-xsmoketest/scripts/cli/test_system_login.py400
-rwxr-xr-xsmoketest/scripts/cli/test_system_logs.py19
-rwxr-xr-xsmoketest/scripts/cli/test_system_option.py66
-rwxr-xr-xsmoketest/scripts/cli/test_system_resolvconf.py6
-rwxr-xr-xsmoketest/scripts/cli/test_system_sflow.py41
-rwxr-xr-xsmoketest/scripts/cli/test_system_syslog.py561
-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.py664
-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.py608
114 files changed, 14700 insertions, 2140 deletions
diff --git a/smoketest/scripts/cli/base_accel_ppp_test.py b/smoketest/scripts/cli/base_accel_ppp_test.py
index c6f6cb804..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
@@ -14,6 +14,7 @@
import re
+from time import sleep
from base_vyostest_shim import VyOSUnitTestSHIM
from configparser import ConfigParser
@@ -25,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"
@@ -40,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
@@ -50,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)
@@ -60,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")
@@ -116,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
@@ -641,6 +651,11 @@ delegate={delegate_2_prefix},{delegate_mask},name={pool_name}"""
for log_level in range(0, 5):
self.set(['log', 'level', str(log_level)])
self.cli_commit()
+
+ # Systemd comes with a default of 5 restarts in 10 seconds policy,
+ # this limit can be hit by this reastart sequence, slow down a bit
+ sleep(5)
+
# Validate configuration values
conf = ConfigParser(allow_no_value=True)
conf.read(self._config_file)
diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py
index 593b4b415..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-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,10 +13,13 @@
# 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
@@ -37,12 +40,15 @@ from vyos.utils.network import is_intf_addr_assigned
from vyos.utils.network import is_ipv6_link_local
from vyos.utils.network import get_nft_vrf_zone_mapping
from vyos.xml_ref import cli_defined
+from vyos.xml_ref import default_value
dhclient_base_dir = directories['isc_dhclient_dir']
dhclient_process_name = 'dhclient'
dhcp6c_base_dir = directories['dhcp6_client_dir']
dhcp6c_process_name = 'dhcp6c'
+MSG_TESTCASE_UNSUPPORTED = 'unsupported on interface family'
+
server_ca_root_cert_data = """
MIIBcTCCARagAwIBAgIUDcAf1oIQV+6WRaW7NPcSnECQ/lUwCgYIKoZIzj0EAwIw
HjEcMBoGA1UEAwwTVnlPUyBzZXJ2ZXIgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjBa
@@ -116,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
@@ -127,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
@@ -155,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'
@@ -175,6 +181,7 @@ 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:
@@ -211,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
@@ -236,7 +246,7 @@ class BasicInterfaceTest:
def test_dhcp_client_options(self):
if not self._test_dhcp or not self._test_vrf:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
client_id = 'VyOS-router'
distance = '100'
@@ -276,7 +286,10 @@ class BasicInterfaceTest:
def test_dhcp_vrf(self):
if not self._test_dhcp or not self._test_vrf:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
+
+ cli_default_metric = default_value(self._base_path + [self._interfaces[0],
+ 'dhcp-options', 'default-route-distance'])
vrf_name = 'purple4'
self.cli_set(['vrf', 'name', vrf_name, 'table', '65000'])
@@ -303,13 +316,34 @@ class BasicInterfaceTest:
self.assertIn(str(dhclient_pid), vrf_pids)
# and the commandline has the appropriate options
cmdline = read_file(f'/proc/{dhclient_pid}/cmdline')
- self.assertIn('-e\x00IF_METRIC=210', cmdline) # 210 is the default value
+ self.assertIn(f'-e\x00IF_METRIC={cli_default_metric}', cmdline)
+
+ # T5103: remove interface from VRF instance and move DHCP client
+ # back to default VRF. This must restart the DHCP client process
+ for interface in self._interfaces:
+ self.cli_delete(self._base_path + [interface, 'vrf'])
+
+ self.cli_commit()
+
+ # Validate interface state
+ for interface in self._interfaces:
+ tmp = get_interface_vrf(interface)
+ self.assertEqual(tmp, 'default')
+ # Check if dhclient process runs
+ dhclient_pid = process_named_running(dhclient_process_name, cmdline=interface, timeout=10)
+ self.assertTrue(dhclient_pid)
+ # .. inside the appropriate VRF instance
+ vrf_pids = cmd(f'ip vrf pids {vrf_name}')
+ self.assertNotIn(str(dhclient_pid), vrf_pids)
+ # and the commandline has the appropriate options
+ cmdline = read_file(f'/proc/{dhclient_pid}/cmdline')
+ self.assertIn(f'-e\x00IF_METRIC={cli_default_metric}', cmdline)
self.cli_delete(['vrf', 'name', vrf_name])
def test_dhcpv6_vrf(self):
if not self._test_ipv6_dhcpc6 or not self._test_vrf:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
vrf_name = 'purple6'
self.cli_set(['vrf', 'name', vrf_name, 'table', '65001'])
@@ -337,11 +371,31 @@ class BasicInterfaceTest:
vrf_pids = cmd(f'ip vrf pids {vrf_name}')
self.assertIn(str(tmp), vrf_pids)
+ # T7135: remove interface from VRF instance and move DHCP client
+ # back to default VRF. This must restart the DHCP client process
+ for interface in self._interfaces:
+ self.cli_delete(self._base_path + [interface, 'vrf'])
+
+ self.cli_commit()
+
+ # Validate interface state
+ for interface in self._interfaces:
+ tmp = get_interface_vrf(interface)
+ self.assertEqual(tmp, 'default')
+
+ # Check if dhclient process runs
+ tmp = process_named_running(dhcp6c_process_name, cmdline=interface, timeout=10)
+ self.assertTrue(tmp)
+ # .. inside the appropriate VRF instance
+ vrf_pids = cmd(f'ip vrf pids {vrf_name}')
+ self.assertNotIn(str(tmp), vrf_pids)
+
+
self.cli_delete(['vrf', 'name', vrf_name])
def test_move_interface_between_vrf_instances(self):
if not self._test_vrf:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
vrf1_name = 'smoketest_mgmt1'
vrf1_table = '5424'
@@ -386,7 +440,7 @@ class BasicInterfaceTest:
def test_add_to_invalid_vrf(self):
if not self._test_vrf:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
# move interface into first VRF
for interface in self._interfaces:
@@ -403,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:
@@ -420,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
@@ -511,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]
@@ -544,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]
@@ -563,8 +633,8 @@ class BasicInterfaceTest:
def test_mtu_1200_no_ipv6_interface(self):
# Testcase if MTU can be changed to 1200 on non IPv6
# enabled interfaces
- if not self._test_mtu:
- self.skipTest('not supported')
+ if not self._test_mtu or not self._test_ipv6:
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
old_mtu = self._mtu
self._mtu = '1200'
@@ -600,7 +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]
@@ -645,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):
@@ -671,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):
@@ -691,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]
@@ -761,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]
@@ -801,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]
@@ -868,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]
@@ -906,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'
@@ -1008,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]
@@ -1036,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:
@@ -1067,13 +1144,20 @@ class BasicInterfaceTest:
self.assertIn('fib saddr . iif oif 0', line)
self.assertIn('drop', line)
+ if cli_defined(self._base_path + ['ipv6', 'address'], 'interface-identifier'):
+ tmp = cmd(f'ip -j token show dev {interface}')
+ tmp = loads(tmp)[0]
+ self.assertEqual(tmp['token'], interface_identifier)
+ self.assertEqual(tmp['ifname'], interface)
+
+
def test_dhcpv6_client_options(self):
if not self._test_ipv6_dhcpc6:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
duid_base = 10
for interface in self._interfaces:
- duid = '00:01:00:01:27:71:db:f0:00:50:00:00:00:{}'.format(duid_base)
+ duid = f'00:01:00:01:27:71:db:f0:00:50:00:00:00:{duid_base}'
path = self._base_path + [interface]
for option in self._options.get(interface, []):
self.cli_set(path + option.split())
@@ -1090,7 +1174,7 @@ class BasicInterfaceTest:
duid_base = 10
for interface in self._interfaces:
- duid = '00:01:00:01:27:71:db:f0:00:50:00:00:00:{}'.format(duid_base)
+ duid = f'00:01:00:01:27:71:db:f0:00:50:00:00:00:{duid_base}'
dhcpc6_config = read_file(f'{dhcp6c_base_dir}/dhcp6c.{interface}.conf')
self.assertIn(f'interface {interface} ' + '{', dhcpc6_config)
self.assertIn(f' request domain-name-servers;', dhcpc6_config)
@@ -1102,16 +1186,25 @@ class BasicInterfaceTest:
self.assertIn('};', dhcpc6_config)
duid_base += 1
+ # T7058: verify daemon has no problems understanding the custom DUID option
+ j = journal.Reader()
+ j.this_boot()
+ j.add_match(_SYSTEMD_UNIT=f'dhcp6c@{interface}.service')
+ for entry in j:
+ self.assertNotIn('yyerror0', entry.get('MESSAGE', ''))
+ self.assertNotIn('syntax error', entry.get('MESSAGE', ''))
+
# Better ask the process about it's commandline in the future
pid = process_named_running(dhcp6c_process_name, cmdline=interface, timeout=10)
self.assertTrue(pid)
+ # DHCPv6 option "no-release" requires "-n" daemon startup option
dhcp6c_options = read_file(f'/proc/{pid}/cmdline')
self.assertIn('-n', dhcp6c_options)
def test_dhcpv6pd_auto_sla_id(self):
if not self._test_ipv6_pd:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
prefix_len = '56'
sla_len = str(64 - int(prefix_len))
@@ -1154,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)
@@ -1172,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))
@@ -1220,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)
@@ -1238,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 940306ac3..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,18 +13,22 @@
# 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'
@@ -36,39 +40,57 @@ save_config = '/tmp/vyos-smoketest-save'
# 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
+ 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 cli_set(self, config):
+ 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:
- print('set ' + ' '.join(config))
- self._session.set(config)
+ str = f'set {" ".join(path)} {value}' if value else f'set {" ".join(path)}'
+ print(str)
+ self._session.set(path, value)
def cli_delete(self, config):
if self.debug:
@@ -83,10 +105,18 @@ class VyOSUnitTestSHIM:
def cli_commit(self):
if self.debug:
print('commit')
- self._session.commit()
- # during a commit there is a process opening commit_lock, and run() returns 0
+ # During a commit there is a process opening commit_lock, and run()
+ # returns 0
while run(f'sudo lsof -nP {commit_lock}') == 0:
sleep(0.250)
+ # Return the output of commit
+ # Necessary for testing Warning cases
+ return self._session.commit()
+
+ def cli_save(self, file):
+ if self.debug:
+ print('save')
+ self._session.save_config(file)
def op_mode(self, path : list) -> None:
"""
@@ -101,23 +131,79 @@ class VyOSUnitTestSHIM:
pprint.pprint(out)
return out
- def getFRRconfig(self, string=None, end='$', endsection='^!', daemon=''):
- """ Retrieve current "running configuration" from FRR """
- command = f'vtysh -c "show run {daemon} no-header"'
- if string: command += f' | sed -n "/^{string}{end}/,/{endsection}/p"'
- out = cmd(command)
+ 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
+
+ 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")
+ """
+ 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)
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()
@@ -125,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:
@@ -136,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:
@@ -147,6 +352,98 @@ class VyOSUnitTestSHIM:
break
self.assertTrue(not matched if inverse else matched, msg=search)
+ 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:
+ self.fail(f'Chain exists: {table} {chain}')
+ except OSError:
+ if not inverse:
+ self.fail(f'Chain does not exist: {table} {chain}')
+
+ # Verify ip rule output
+ def verify_rules(self, rules_search, inverse=False, addr_family='inet'):
+ rule_output = cmd(f'ip -family {addr_family} rule show')
+
+ for search in rules_search:
+ matched = False
+ for line in rule_output.split("\n"):
+ if all(item in line for item in search):
+ matched = True
+ break
+ self.assertTrue(not matched if inverse else matched, msg=search)
+
+ @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 c03b9eb44..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-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,6 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import os
import unittest
import glob
import json
@@ -22,14 +23,15 @@ 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
base_path = ['container']
-cont_image = 'busybox:stable' # busybox is included in vyos-build
PROCESS_NAME = 'conmon'
PROCESS_PIDFILE = '/run/vyos-container-{0}.service.pid'
+busybox_image = 'busybox:stable'
busybox_image_path = '/usr/share/vyos/busybox-stable.tar'
def cmd_to_json(command):
@@ -42,22 +44,21 @@ class TestContainer(VyOSUnitTestSHIM.TestCase):
def setUpClass(cls):
super(TestContainer, cls).setUpClass()
- # Load image for smoketest provided in vyos-build
- try:
- cmd(f'cat {busybox_image_path} | sudo podman load')
- except:
- cls.skipTest(cls, reason='busybox image not available')
+ # Load image for smoketest provided in vyos-1x-smoketest
+ if not os.path.exists(busybox_image_path):
+ cls.fail(cls, f'{busybox_image} image not available')
+ cmd(f'sudo podman load -i {busybox_image_path}')
# ensure we can also run this test on a live system - so lets clean
# out the current configuration :)
cls.cli_delete(cls, base_path)
+ cls.cli_delete(cls, ['vrf'])
@classmethod
def tearDownClass(cls):
super(TestContainer, cls).tearDownClass()
-
# Cleanup podman image
- cmd(f'sudo podman image rm -f {cont_image}')
+ cmd(f'sudo podman image rm -f {busybox_image}')
def tearDown(self):
self.cli_delete(base_path)
@@ -69,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', cont_image])
+ self.cli_set(base_path + ['name', cont_name, 'image', busybox_image])
self.cli_set(base_path + ['name', cont_name, 'allow-host-networks'])
- self.cli_set(base_path + ['name', cont_name, 'sysctl', 'parameter', 'kernel.msgmax', 'value', '4096'])
-
+ self.cli_set(
+ base_path
+ + [
+ 'name',
+ cont_name,
+ 'sysctl',
+ 'parameter',
+ 'kernel.msgmax',
+ 'value',
+ '4096',
+ ]
+ )
+ self.cli_set(base_path + ['name', cont_name, 'log-driver', 'journald'])
# commit changes
self.cli_commit()
@@ -96,11 +112,70 @@ 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_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])
+ 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):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['network', net_name, 'no-name-server'])
+ self.cli_commit()
+
+ n = cmd_to_json(f'sudo podman inspect {cont_name}')
+ self.assertEqual(n['HostConfig']['Dns'], name_servers)
+
+ tmp = cmd(f'sudo podman exec -it {cont_name} cat /etc/resolv.conf')
+ self.assertIn(name_server, tmp)
+
def test_cpu_limit(self):
cont_name = 'c2'
self.cli_set(base_path + ['name', cont_name, 'allow-host-networks'])
- self.cli_set(base_path + ['name', cont_name, 'image', cont_image])
+ self.cli_set(base_path + ['name', cont_name, 'image', busybox_image])
self.cli_set(base_path + ['name', cont_name, 'cpu-quota', '1.25'])
self.cli_commit()
@@ -112,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'
@@ -121,8 +273,18 @@ class TestContainer(VyOSUnitTestSHIM.TestCase):
for ii in range(1, 6):
name = f'{base_name}-{ii}'
- self.cli_set(base_path + ['name', name, 'image', cont_image])
- self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + ii)])
+ self.cli_set(base_path + ['name', name, 'image', busybox_image])
+ self.cli_set(
+ base_path
+ + [
+ 'name',
+ name,
+ 'network',
+ net_name,
+ 'address',
+ str(ip_interface(prefix).ip + ii),
+ ]
+ )
# verify() - first IP address of a prefix can not be used by a container
with self.assertRaises(ConfigSessionError):
@@ -135,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'
@@ -151,8 +319,18 @@ class TestContainer(VyOSUnitTestSHIM.TestCase):
for ii in range(1, 6):
name = f'{base_name}-{ii}'
- self.cli_set(base_path + ['name', name, 'image', cont_image])
- self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + ii)])
+ self.cli_set(base_path + ['name', name, 'image', busybox_image])
+ self.cli_set(
+ base_path
+ + [
+ 'name',
+ name,
+ 'network',
+ net_name,
+ 'address',
+ str(ip_interface(prefix).ip + ii),
+ ]
+ )
# verify() - first IP address of a prefix can not be used by a container
with self.assertRaises(ConfigSessionError):
@@ -165,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'
@@ -183,9 +367,29 @@ class TestContainer(VyOSUnitTestSHIM.TestCase):
for ii in range(1, 6):
name = f'{base_name}-{ii}'
- self.cli_set(base_path + ['name', name, 'image', cont_image])
- self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix4).ip + ii)])
- self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix6).ip + ii)])
+ self.cli_set(base_path + ['name', name, 'image', busybox_image])
+ self.cli_set(
+ base_path
+ + [
+ 'name',
+ name,
+ 'network',
+ net_name,
+ 'address',
+ str(ip_interface(prefix4).ip + ii),
+ ]
+ )
+ self.cli_set(
+ base_path
+ + [
+ 'name',
+ name,
+ 'network',
+ net_name,
+ 'address',
+ str(ip_interface(prefix6).ip + ii),
+ ]
+ )
# verify() - first IP address of a prefix can not be used by a container
with self.assertRaises(ConfigSessionError):
@@ -199,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'
@@ -217,20 +433,56 @@ class TestContainer(VyOSUnitTestSHIM.TestCase):
self.cli_set(base_path + ['network', net_name, 'no-name-server'])
name = f'{base_name}-2'
- self.cli_set(base_path + ['name', name, 'image', cont_image])
- self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + 2)])
+ self.cli_set(base_path + ['name', name, 'image', busybox_image])
+ self.cli_set(
+ base_path
+ + [
+ 'name',
+ name,
+ 'network',
+ net_name,
+ 'address',
+ str(ip_interface(prefix).ip + 2),
+ ]
+ )
self.cli_commit()
n = cmd_to_json(f'sudo podman network inspect {net_name}')
self.assertEqual(n['dns_enabled'], False)
+ def test_network_mtu(self):
+ prefix = '192.0.2.0/24'
+ base_name = 'ipv4'
+ net_name = 'NET01'
+
+ self.cli_set(base_path + ['network', net_name, 'prefix', prefix])
+ self.cli_set(base_path + ['network', net_name, 'mtu', '1280'])
+
+ name = f'{base_name}-2'
+ self.cli_set(base_path + ['name', name, 'image', busybox_image])
+ self.cli_set(
+ base_path
+ + [
+ 'name',
+ name,
+ 'network',
+ net_name,
+ 'address',
+ str(ip_interface(prefix).ip + 2),
+ ]
+ )
+ self.cli_commit()
+
+ n = cmd_to_json(f'sudo podman network inspect {net_name}')
+ self.assertEqual(n['options']['mtu'], '1280')
+
def test_uid_gid(self):
cont_name = 'uid-test'
gid = '100'
uid = '1001'
self.cli_set(base_path + ['name', cont_name, 'allow-host-networks'])
- self.cli_set(base_path + ['name', cont_name, 'image', cont_image])
+ self.cli_set(base_path + ['name', cont_name, 'image', busybox_image])
self.cli_set(base_path + ['name', cont_name, 'gid', gid])
# verify() - GID can only be set if UID is set
@@ -252,17 +504,44 @@ class TestContainer(VyOSUnitTestSHIM.TestCase):
for ii in container_list:
name = f'{base_name}-{ii}'
- self.cli_set(base_path + ['name', name, 'image', cont_image])
+ self.cli_set(base_path + ['name', name, 'image', busybox_image])
self.cli_set(base_path + ['name', name, 'allow-host-networks'])
self.cli_commit()
# Query API about running containers
- tmp = cmd("sudo curl --unix-socket /run/podman/podman.sock -H 'content-type: application/json' -sf http://localhost/containers/json")
+ tmp = cmd(
+ "sudo curl --unix-socket /run/podman/podman.sock -H 'content-type: application/json' -sf http://localhost/containers/json"
+ )
tmp = json.loads(tmp)
# We expect the same amount of containers from the API that we started above
self.assertEqual(len(container_list), len(tmp))
+ 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 3e9ec2935..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-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
@@ -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
@@ -119,6 +121,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.com'])
self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.org'])
self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'eth0'])
+ self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'pod-smoketest'])
self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'vtun0'])
self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'accept'])
@@ -133,6 +136,9 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'action', 'accept'])
self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'outbound-interface', 'group', '!smoketest_interface'])
+ # Create container network so test won't fail
+ self.cli_set(['container', 'network', 'smoketest', 'prefix', '10.0.0.0/24'])
+
self.cli_commit()
self.wait_for_domain_resolver('ip vyos_filter', 'D_smoketest_domain', '192.0.2.5')
@@ -248,6 +254,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'action', 'notrack'])
self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'protocol', 'tcp'])
self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'destination', 'port', '23'])
+ self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'set', 'mark', '55'])
self.cli_commit()
@@ -275,7 +282,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
['OUT-raw default-action drop', 'drop'],
['chain VYOS_PREROUTING_raw'],
['type filter hook prerouting priority raw; policy accept;'],
- ['tcp dport 23', 'notrack'],
+ ['tcp dport 23', 'meta mark set 0x00000037', 'notrack'],
['PRE-raw default-action accept', 'accept'],
['chain NAME_smoketest'],
['saddr 172.16.20.10', 'daddr 172.16.10.10', 'log prefix "[ipv4-NAM-smoketest-1-A]" log level debug', 'ip ttl 15', 'accept'],
@@ -285,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'
@@ -315,6 +349,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'mark', '1010'])
self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'jump'])
self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'jump-target', name])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'set', 'dscp', '32'])
self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '2', 'protocol', 'tcp'])
self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '2', 'mark', '!98765'])
@@ -331,7 +366,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
nftables_search = [
['chain VYOS_FORWARD_filter'],
['type filter hook forward priority filter; policy accept;'],
- ['ip saddr 198.51.100.1-198.51.100.50', 'meta mark 0x000003f2', f'jump NAME_{name}'],
+ ['ip saddr 198.51.100.1-198.51.100.50', 'meta mark 0x000003f2', 'ip dscp set cs4', f'jump NAME_{name}'],
['FWD-filter default-action drop', 'drop'],
['chain VYOS_INPUT_filter'],
['type filter hook input priority filter; policy accept;'],
@@ -395,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')
@@ -405,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'])
@@ -427,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')
@@ -485,6 +530,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.cli_set(['firewall', 'ipv6', 'prerouting', 'raw', 'rule', '1', 'action', 'drop'])
self.cli_set(['firewall', 'ipv6', 'prerouting', 'raw', 'rule', '1', 'protocol', 'tcp'])
self.cli_set(['firewall', 'ipv6', 'prerouting', 'raw', 'rule', '1', 'destination', 'port', '23'])
+ self.cli_set(['firewall', 'ipv6', 'prerouting', 'raw', 'rule', '1', 'set', 'hop-limit', '79'])
self.cli_commit()
@@ -507,7 +553,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
['OUT-raw default-action drop', 'drop'],
['chain VYOS_IPV6_PREROUTING_raw'],
['type filter hook prerouting priority raw; policy accept;'],
- ['tcp dport 23', 'drop'],
+ ['tcp dport 23', 'ip6 hoplimit set 79', 'drop'],
['PRE-raw default-action accept', 'accept'],
[f'chain NAME6_{name}'],
['saddr 2002::1-2002::10', 'daddr 2002::1:1', 'log prefix "[ipv6-NAM-v6-smoketest-1-A]" log level crit', 'accept'],
@@ -521,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'
@@ -596,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'])
@@ -618,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'])
@@ -644,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']
@@ -651,6 +741,13 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.verify_nftables(nftables_search, 'ip vyos_filter')
+ # T7148 - Ensure bridge rule reject -> drop
+ self.cli_set(['firewall', 'global-options', 'state-policy', 'invalid', 'action', 'reject'])
+ self.cli_commit()
+
+ self.verify_nftables([['ct state invalid', 'reject']], 'ip vyos_filter')
+ self.verify_nftables([['ct state invalid', 'drop']], 'bridge vyos_filter')
+
# Check conntrack is enabled from state-policy
self.verify_nftables_chain([['accept']], 'ip vyos_conntrack', 'FW_CONNTRACK')
self.verify_nftables_chain([['accept']], 'ip6 vyos_conntrack', 'FW_CONNTRACK')
@@ -707,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'])
@@ -722,9 +825,12 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '1', 'action', 'accept'])
self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '1', 'vlan', 'id', vlan_id])
self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '1', 'vlan', 'ethernet-type', 'ipv4'])
+ self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '1', 'set', 'connection-mark', '123123'])
+
self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'action', 'jump'])
self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'jump-target', name])
self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'vlan', 'priority', vlan_prior])
+ self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'set', 'ttl', '128'])
self.cli_set(['firewall', 'bridge', 'input', 'filter', 'rule', '1', 'action', 'accept'])
self.cli_set(['firewall', 'bridge', 'input', 'filter', 'rule', '1', 'inbound-interface', 'name', interface_in])
@@ -746,8 +852,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
['chain VYOS_FORWARD_filter'],
['type filter hook forward priority filter; policy accept;'],
['jump VYOS_STATE_POLICY'],
- [f'vlan id {vlan_id}', 'vlan type ip', 'accept'],
- [f'vlan pcp {vlan_prior}', f'jump NAME_{name}'],
+ [f'vlan id {vlan_id}', 'vlan type ip', 'ct mark set 0x0001e0f3', 'accept'],
+ [f'vlan pcp {vlan_prior}', 'ip ttl set 128', f'jump NAME_{name}'],
['log prefix "[bri-FWD-filter-default-D]"', 'drop', 'FWD-filter default-action drop'],
[f'chain NAME_{name}'],
[f'ether saddr {mac_address}', f'iifname "{interface_in}"', f'log prefix "[bri-NAM-{name}-1-A]" log level crit', 'accept'],
@@ -759,6 +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'],
@@ -899,7 +1010,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
def test_zone_basic(self):
self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'default-action', 'drop'])
self.cli_set(['firewall', 'ipv6', 'name', 'smoketestv6', 'default-action', 'drop'])
- self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'interface', 'eth0'])
+ self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'member', 'interface', 'eth0'])
self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'from', 'smoketest-local', 'firewall', 'name', 'smoketest'])
self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'intra-zone-filtering', 'firewall', 'ipv6-name', 'smoketestv6'])
self.cli_set(['firewall', 'zone', 'smoketest-local', 'local-zone'])
@@ -909,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 = [
@@ -957,6 +1082,179 @@ 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'])
+ self.cli_set(['firewall', 'ipv6', 'name', 'LOCAL_to_ZONE2_v6', 'default-action', 'drop'])
+ self.cli_set(['firewall', 'zone', 'LOCAL', 'from', 'ZONE1', 'firewall', 'name', 'ZONE1-to-LOCAL'])
+ self.cli_set(['firewall', 'zone', 'LOCAL', 'local-zone'])
+ self.cli_set(['firewall', 'zone', 'ZONE1', 'from', 'ZONE2', 'firewall', 'name', 'ZONE2_to_ZONE1'])
+ self.cli_set(['firewall', 'zone', 'ZONE1', 'member', 'interface', 'eth1'])
+ self.cli_set(['firewall', 'zone', 'ZONE1', 'member', 'interface', 'eth2'])
+ self.cli_set(['firewall', 'zone', 'ZONE1', 'member', 'vrf', 'VRF-1'])
+ self.cli_set(['firewall', 'zone', 'ZONE2', 'from', 'LOCAL', 'firewall', 'ipv6-name', 'LOCAL_to_ZONE2_v6'])
+ self.cli_set(['firewall', 'zone', 'ZONE2', 'member', 'interface', 'vtun66'])
+ self.cli_set(['firewall', 'zone', 'ZONE2', 'member', 'vrf', 'VRF-2'])
+
+ self.cli_set(['vrf', 'name', 'VRF-1', 'table', '101'])
+ self.cli_set(['vrf', 'name', 'VRF-2', 'table', '102'])
+ self.cli_set(['interfaces', 'ethernet', 'eth0', 'vrf', 'VRF-1'])
+ self.cli_set(['interfaces', 'vti', 'vti1', 'vrf', 'VRF-2'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['chain NAME_ZONE1-to-LOCAL'],
+ ['counter', 'accept', 'comment "NAM-ZONE1-to-LOCAL default-action accept"'],
+ ['chain NAME_ZONE2_to_ZONE1'],
+ ['counter', 'continue', 'comment "NAM-ZONE2_to_ZONE1 default-action continue"'],
+ ['chain VYOS_ZONE_FORWARD'],
+ ['type filter hook forward priority filter + 1'],
+ ['oifname { "eth1", "eth2" }', 'counter packets', 'jump VZONE_ZONE1'],
+ ['oifname "VRF-1"', 'counter packets', 'jump VZONE_ZONE1'],
+ ['oifname "vtun66"', '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'],
+ ['chain VYOS_ZONE_OUTPUT'],
+ ['type filter hook output priority filter + 1'],
+ ['counter packets', 'jump VZONE_LOCAL_OUT'],
+ ['chain VZONE_LOCAL_IN'],
+ ['iifname { "eth1", "eth2" }', 'counter packets', 'jump NAME_ZONE1-to-LOCAL'],
+ ['iifname "VRF-1"', 'counter packets', 'jump NAME_ZONE1-to-LOCAL'],
+ ['counter packets', 'drop', 'comment "zone_LOCAL default-action drop"'],
+ ['chain VZONE_LOCAL_OUT'],
+ ['counter packets', 'drop', 'comment "zone_LOCAL default-action drop"'],
+ ['chain VZONE_ZONE1'],
+ ['iifname { "eth1", "eth2" }', 'counter packets', 'return'],
+ ['iifname "VRF-1"', 'counter packets', 'return'],
+ ['iifname "vtun66"', 'counter packets', 'jump NAME_ZONE2_to_ZONE1'],
+ ['iifname "vtun66"', 'counter packets', 'return'],
+ ['iifname "VRF-2"', 'counter packets', 'jump NAME_ZONE2_to_ZONE1'],
+ ['iifname "VRF-2"', 'counter packets', 'return'],
+ ['counter packets', 'drop', 'comment "zone_ZONE1 default-action drop"'],
+ ['chain VZONE_ZONE2'],
+ ['iifname "vtun66"', 'counter packets', 'return'],
+ ['iifname "VRF-2"', 'counter packets', 'return'],
+ ['counter packets', 'drop', 'comment "zone_ZONE2 default-action drop"']
+ ]
+
+ nftables_search_v6 = [
+ ['chain NAME6_LOCAL_to_ZONE2_v6'],
+ ['counter', 'drop', 'comment "NAM-LOCAL_to_ZONE2_v6 default-action drop"'],
+ ['chain VYOS_ZONE_FORWARD'],
+ ['type filter hook forward priority filter + 1'],
+ ['oifname { "eth1", "eth2" }', 'counter packets', 'jump VZONE_ZONE1'],
+ ['oifname "VRF-1"', 'counter packets', 'jump VZONE_ZONE1'],
+ ['oifname "vtun66"', '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'],
+ ['chain VYOS_ZONE_OUTPUT'],
+ ['type filter hook output priority filter + 1'],
+ ['counter packets', 'jump VZONE_LOCAL_OUT'],
+ ['chain VZONE_LOCAL_IN'],
+ ['counter packets', 'drop', 'comment "zone_LOCAL default-action drop"'],
+ ['chain VZONE_LOCAL_OUT'],
+ ['oifname "vtun66"', 'counter packets', 'jump NAME6_LOCAL_to_ZONE2_v6'],
+ ['oifname "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'],
+ ['iifname "VRF-1"', 'counter packets', 'return'],
+ ['counter packets', 'drop', 'comment "zone_ZONE1 default-action drop"'],
+ ['chain VZONE_ZONE2'],
+ ['iifname "vtun66"', 'counter packets', 'return'],
+ ['iifname "VRF-2"', 'counter packets', 'return'],
+ ['counter packets', 'drop', 'comment "zone_ZONE2 default-action drop"']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_filter')
+ self.verify_nftables(nftables_search_v6, 'ip6 vyos_filter')
+
+ def test_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'])
@@ -996,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'])
@@ -1068,7 +1372,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4'])
self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'action', 'jump'])
self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4'])
-
+
self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'action', 'jump'])
self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-out4'])
self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'jump'])
@@ -1103,8 +1407,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'action', 'jump'])
self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'jump-target', 'smoketest-cycle-1'])
- # nft will fail to load cyclic jumps in any form, whether the rule is reachable or not.
- # It should be caught by conf validation.
+ # nft will fail to load cyclic jumps in any form, whether the rule is reachable or not.
+ # It should be caught by conf validation.
with self.assertRaises(ConfigSessionError):
self.cli_commit()
@@ -1163,5 +1467,163 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
with self.assertRaises(ConfigSessionError):
self.cli_commit()
+ def test_ipv4_remote_group(self):
+ # Setup base config for test
+ self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'url', 'http://127.0.0.1:80/list.txt'])
+ self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'description', 'Example Group 01'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'destination', 'group', 'remote-group', 'group01'])
+
+ self.cli_commit()
+
+ # Test remote-group had been loaded correctly in nft
+ nftables_search = [
+ ['R_group01'],
+ ['type ipv4_addr'],
+ ['flags interval'],
+ ['meta l4proto', 'daddr @R_group01', 'ipv4-INP-filter-10']
+ ]
+ self.verify_nftables(nftables_search, 'ip vyos_filter')
+
+ # Test remote-group cannot be configured without a URL
+ self.cli_delete(['firewall', 'group', 'remote-group', 'group01', 'url'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_discard()
+
+ # Test remote-group cannot be set alongside address in rules
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'destination', 'address', '127.0.0.1'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_discard()
+
+
+ def test_ipv6_remote_group(self):
+ # Setup base config for test
+ self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'url', 'http://127.0.0.1:80/list.txt'])
+ self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'description', 'Example Group 01'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'destination', 'group', 'remote-group', 'group01'])
+
+ self.cli_commit()
+
+ # Test remote-group had been loaded correctly in nft
+ nftables_search = [
+ ['R6_group01'],
+ ['type ipv6_addr'],
+ ['flags interval'],
+ ['meta l4proto', 'daddr @R6_group01', 'ipv6-INP-filter-10']
+ ]
+ self.verify_nftables(nftables_search, 'ip6 vyos_filter')
+
+ # Test remote-group cannot be configured without a URL
+ self.cli_delete(['firewall', 'group', 'remote-group', 'group01', 'url'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_discard()
+
+ # Test remote-group cannot be set alongside address in rules
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'destination', 'address', '2001:db8::1'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_discard()
+
+
+ def test_remote_group(self):
+ # Setup base config for test adding remote group to both ipv4 and ipv6 rules
+ self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'url', 'http://127.0.0.1:80/list.txt'])
+ self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'description', 'Example Group 01'])
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '10', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '10', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '10', 'destination', 'group', 'remote-group', 'group01'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'source', 'group', 'remote-group', 'group01'])
+ self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '10', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '10', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '10', 'destination', 'group', 'remote-group', 'group01'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'source', 'group', 'remote-group', 'group01'])
+
+ self.cli_commit()
+
+ # Test remote-group had been loaded correctly in nft ip table
+ nftables_v4_search = [
+ ['R_group01'],
+ ['type ipv4_addr'],
+ ['flags interval'],
+ ['meta l4proto', 'daddr @R_group01', 'ipv4-OUT-filter-10'],
+ ['meta l4proto', 'saddr @R_group01', 'ipv4-INP-filter-10'],
+ ]
+ self.verify_nftables(nftables_v4_search, 'ip vyos_filter')
+
+ # Test remote-group had been loaded correctly in nft ip6 table
+ nftables_v6_search = [
+ ['R6_group01'],
+ ['type ipv6_addr'],
+ ['flags interval'],
+ ['meta l4proto', 'daddr @R6_group01', 'ipv6-OUT-filter-10'],
+ ['meta l4proto', 'saddr @R6_group01', 'ipv6-INP-filter-10'],
+ ]
+ self.verify_nftables(nftables_v6_search, 'ip6 vyos_filter')
+
+
+ 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 f436424b8..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
@@ -167,18 +214,25 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase):
def test_bonding_multi_use_member(self):
# Define available bonding hash policies
- for interface in ['bond10', 'bond20']:
+ bonds = ['bond10', 'bond20', 'bond30']
+ for interface in bonds:
for member in self._members:
self.cli_set(self._base_path + [interface, 'member', 'interface', member])
# check validate() - can not use the same member interfaces multiple times
with self.assertRaises(ConfigSessionError):
self.cli_commit()
-
- self.cli_delete(self._base_path + ['bond20'])
+ # only keep the first bond interface configuration
+ for interface in bonds[1:]:
+ self.cli_delete(self._base_path + [interface])
self.cli_commit()
+ bond = bonds[0]
+ member_ifaces = read_file(f'/sys/class/net/{bond}/bonding/slaves').split()
+ for member in self._members:
+ self.assertIn(member, member_ifaces)
+
def test_bonding_source_interface(self):
# Re-use member interface that is already a source-interface
bond = 'bond99'
@@ -286,7 +340,7 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase):
id = '5'
for interface in self._interfaces:
- frrconfig = self.getFRRconfig(f'interface {interface}', daemon='zebra')
+ frrconfig = self.getFRRconfig(f'interface {interface}', stop_section='^exit')
self.assertIn(f' evpn mh es-id {id}', frrconfig)
self.assertIn(f' evpn mh es-df-pref {id}', frrconfig)
@@ -303,11 +357,35 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase):
id = '5'
for interface in self._interfaces:
- frrconfig = self.getFRRconfig(f'interface {interface}', daemon='zebra')
+ frrconfig = self.getFRRconfig(f'interface {interface}', 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 124c1fbcb..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,13 @@
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
from vyos.utils.process import cmd
@@ -33,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 (.)
@@ -55,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):
@@ -157,6 +158,21 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase):
# verify member is assigned to the bridge
self.assertEqual(interface, tmp['master'])
+ def test_bridge_multi_use_member(self):
+ # Define available bonding hash policies
+ bridges = ['br10', 'br20', 'br30']
+ for interface in bridges:
+ for member in self._members:
+ self.cli_set(self._base_path + [interface, 'member', 'interface', member])
+
+ # check validate() - can not use the same member interfaces multiple times
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ # only keep the first bond interface configuration
+ for interface in bridges[1:]:
+ self.cli_delete(self._base_path + [interface])
+
+ self.cli_commit()
def test_add_remove_bridge_member(self):
# Add member interfaces to bridge and set STP cost/priority
@@ -460,5 +476,63 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase):
tmp = get_interface_config(interface)
self.assertEqual(protocol, tmp['linkinfo']['info_data']['vlan_protocol'])
+ def test_bridge_delete_with_vxlan_heighbor_suppress(self):
+ vxlan_if = 'vxlan0'
+ vni = '123'
+ br_if = 'br0'
+ eth0_addr = '192.0.2.2/30'
+
+ self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', eth0_addr])
+ self.cli_set(['interfaces', 'vxlan', vxlan_if, 'parameters', 'neighbor-suppress'])
+ self.cli_set(['interfaces', 'vxlan', vxlan_if, 'mtu', '1426'])
+ self.cli_set(['interfaces', 'vxlan', vxlan_if, 'source-address', ip_from_cidr(eth0_addr)])
+ self.cli_set(['interfaces', 'vxlan', vxlan_if, 'vni', vni])
+
+ self.cli_set(['interfaces', 'bridge', br_if, 'member', 'interface', vxlan_if])
+
+ self.cli_commit()
+
+ self.assertTrue(interface_exists(vxlan_if))
+ self.assertTrue(interface_exists(br_if))
+
+ # cannot delete bridge interface if "neighbor-suppress" parameter is configured for VXLAN interface
+ self.cli_delete(['interfaces', 'bridge', br_if])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(['interfaces', 'vxlan', vxlan_if, 'parameters', 'neighbor-suppress'])
+
+ self.cli_commit()
+
+ self.assertFalse(interface_exists(br_if))
+
+ self.cli_delete(['interfaces', 'vxlan', vxlan_if])
+ self.cli_delete(['interfaces', 'ethernet', 'eth0', 'address', eth0_addr])
+
+ 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 3d12364f7..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,23 +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.process import cmd
-from vyos.utils.process import popen
from vyos.utils.file import read_file
+from vyos.utils.network import is_intf_addr_assigned
from vyos.utils.network import is_ipv6_link_local
+from vyos.utils.process import cmd
+from vyos.utils.process import 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:
@@ -77,14 +83,21 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase):
continue
self.assertFalse(is_intf_addr_assigned(interface, addr['addr']))
# Ensure no VLAN interfaces are left behind
- tmp = [x for x in Section.interfaces('ethernet') if x.startswith(f'{interface}.')]
+ tmp = [
+ x
+ for x in Section.interfaces('ethernet')
+ if x.startswith(f'{interface}.')
+ ]
self.assertListEqual(tmp, [])
+ # 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
# expands to rps_cpus = 255 for a 8 core system
- rps_cpus = (1 << os.cpu_count()) -1
+ rps_cpus = (1 << os.cpu_count()) - 1
# XXX: we should probably reserve one core when the system is under
# high preasure so we can still have a core left for housekeeping.
@@ -100,7 +113,7 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase):
for interface in self._interfaces:
cpus = read_file(f'/sys/class/net/{interface}/queues/rx-0/rps_cpus')
# remove the nasty ',' separation on larger strings
- cpus = cpus.replace(',','')
+ cpus = cpus.replace(',', '')
cpus = int(cpus, 16)
self.assertEqual(f'{cpus:x}', f'{rps_cpus:x}')
@@ -116,12 +129,14 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase):
for interface in self._interfaces:
queues = len(glob(f'/sys/class/net/{interface}/queues/rx-*'))
- rfs_flow = int(global_rfs_flow/queues)
+ rfs_flow = int(global_rfs_flow / queues)
for i in range(0, queues):
- tmp = read_file(f'/sys/class/net/{interface}/queues/rx-{i}/rps_flow_cnt')
+ tmp = read_file(
+ f'/sys/class/net/{interface}/queues/rx-{i}/rps_flow_cnt'
+ )
self.assertEqual(int(tmp), rfs_flow)
- tmp = read_file(f'/proc/sys/net/core/rps_sock_flow_entries')
+ tmp = read_file('/proc/sys/net/core/rps_sock_flow_entries')
self.assertEqual(int(tmp), global_rfs_flow)
# delete configuration of RFS and check all values returned to default "0"
@@ -132,23 +147,26 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase):
for interface in self._interfaces:
queues = len(glob(f'/sys/class/net/{interface}/queues/rx-*'))
- rfs_flow = int(global_rfs_flow/queues)
+ rfs_flow = int(global_rfs_flow / queues)
for i in range(0, queues):
- tmp = read_file(f'/sys/class/net/{interface}/queues/rx-{i}/rps_flow_cnt')
+ tmp = read_file(
+ f'/sys/class/net/{interface}/queues/rx-{i}/rps_flow_cnt'
+ )
self.assertEqual(int(tmp), 0)
-
def test_non_existing_interface(self):
- unknonw_interface = self._base_path + ['eth667']
- self.cli_set(unknonw_interface)
+ 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:
@@ -188,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
@@ -212,15 +309,24 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase):
out = loads(out)
self.assertFalse(out[0]['autonegotiate'])
- def test_ethtool_evpn_uplink_tarcking(self):
+ def test_ethtool_evpn_uplink_tracking(self):
for interface in self._interfaces:
self.cli_set(self._base_path + [interface, 'evpn', 'uplink'])
self.cli_commit()
for interface in self._interfaces:
- frrconfig = self.getFRRconfig(f'interface {interface}', daemon='zebra')
- self.assertIn(f' evpn mh uplink', frrconfig)
+ frrconfig = self.getFRRconfig(f'interface {interface}', stop_section='^exit')
+ self.assertIn(' evpn mh uplink', frrconfig)
+
+ def test_switchdev(self):
+ interface = self._interfaces[0]
+ self.cli_set(self._base_path + [interface, 'switchdev'])
+
+ # check validate() - virtual interfaces do not support switchdev
+ # should print out warning that enabling failed
+
+ self.cli_delete(self._base_path + [interface, 'switchdev'])
if __name__ == '__main__':
- unittest.main(verbosity=2)
+ 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 b2076b43b..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,7 +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 base_interfaces_test import BasicInterfaceTest
def convert_to_list(ranges_to_convert):
result_list = []
@@ -114,6 +116,30 @@ class VXLANInterfaceTest(BasicInterfaceTest.TestCase):
self.assertEqual(Interface(interface).get_admin_state(), 'up')
ttl += 10
+
+ def test_vxlan_group_remote_error(self):
+ intf = 'vxlan60'
+ options = [
+ 'group 239.4.4.5',
+ 'mtu 1420',
+ 'remote 192.168.0.254',
+ 'source-address 192.168.0.1',
+ 'source-interface eth0',
+ 'vni 60'
+ ]
+ for option in options:
+ opts = option.split()
+ self.cli_set(self._base_path + [intf] + opts)
+
+ # verify() - Both group and remote cannot be specified
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # Remove blocking CLI option
+ self.cli_delete(self._base_path + [intf, 'group'])
+ self.cli_commit()
+
+
def test_vxlan_external(self):
interface = 'vxlan0'
source_address = '192.0.2.1'
@@ -363,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 4b994a659..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-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,27 +17,35 @@
import os
import unittest
-from base_vyostest_shim import VyOSUnitTestSHIM
+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
+from vyos.utils.process import is_systemd_service_running
base_path = ['interfaces', 'wireguard']
-
-class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase):
+domain_resolver = 'vyos-domain-resolver.service'
+class WireGuardInterfaceTest(BasicInterfaceTest.TestCase):
@classmethod
def setUpClass(cls):
- super(WireGuardInterfaceTest, cls).setUpClass()
-
- cls._test_addr = ['192.0.2.1/26', '192.0.2.255/31', '192.0.2.64/32',
- '2001:db8:1::ffff/64', '2001:db8:101::1/112']
- cls._interfaces = ['wg0', 'wg1']
+ cls._base_path = ['interfaces', 'wireguard']
+ cls._options = {
+ 'wg0': ['private-key wBbGJJXYllwDcw63AFjiIR6ZlsvqvAf3eDwog64Dp0Q=',
+ 'peer RED public-key 6hkkfxN4VUQLu36NLZr47I7ST/FkQl2clPWr+9a6ZH8=',
+ 'peer RED allowed-ips 169.254.0.0/16',
+ 'port 5678'],
+ 'wg1': ['private-key QFwnBHlHYspehvpklBKb7cikM+QMkEy2p6gfsg06S08=',
+ 'peer BLUE public-key hRJLmP8SVU9/MLmPmYmpOa+RTB4F/zhDqA+/QDuW1Hg=',
+ 'peer BLUE allowed-ips 169.254.0.0/16',
+ 'port 4567'],
+ }
+ cls._interfaces = list(cls._options)
- def tearDown(self):
- self.cli_delete(base_path)
- self.cli_commit()
+ super(WireGuardInterfaceTest, cls).setUpClass()
- def test_01_wireguard_peer(self):
+ def test_wireguard_peer(self):
# Create WireGuard interfaces with associated peers
for intf in self._interfaces:
peer = 'foo-' + intf
@@ -64,7 +72,7 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase):
self.assertTrue(os.path.isdir(f'/sys/class/net/{intf}'))
- def test_02_wireguard_add_remove_peer(self):
+ def test_wireguard_add_remove_peer(self):
# T2939: Create WireGuard interfaces with associated peers.
# Remove one of the configured peers.
# T4774: Test prevention of duplicate peer public keys
@@ -102,7 +110,7 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase):
self.cli_delete(base_path + [interface, 'peer', 'PEER01'])
self.cli_commit()
- def test_03_wireguard_same_public_key(self):
+ def test_wireguard_same_public_key(self):
# T5413: Test prevention of equality interface public key and peer's
# public key
interface = 'wg0'
@@ -115,51 +123,48 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase):
self.cli_set(base_path + [interface, 'private-key', privkey])
self.cli_set(base_path + [interface, 'peer', 'PEER01', 'public-key', pubkey_fail])
- self.cli_set(base_path + [interface, 'peer', 'PEER01', 'port', port])
self.cli_set(base_path + [interface, 'peer', 'PEER01', 'allowed-ips', '10.205.212.10/32'])
self.cli_set(base_path + [interface, 'peer', 'PEER01', 'address', '192.0.2.1'])
# The same pubkey as the interface wg0
with self.assertRaises(ConfigSessionError):
self.cli_commit()
-
self.cli_set(base_path + [interface, 'peer', 'PEER01', 'public-key', pubkey_ok])
+ # If address is defined for a peer, so must be the peer port
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + [interface, 'peer', 'PEER01', 'port', port])
+
# Commit peers
self.cli_commit()
self.assertTrue(os.path.isdir(f'/sys/class/net/{interface}'))
- def test_04_wireguard_threaded(self):
+ def test_wireguard_threaded(self):
# T5409: Test adding threaded option on interface.
- # Test prevention for adding threaded
- # if no enabled peer is configured.
- interface = 'wg0'
- port = '12345'
- privkey = 'OOjcXGfgQlAuM6q8Z9aAYduCua7pxf7UKYvIqoUPoGQ='
- pubkey = 'ebFx/1G0ti8tvuZd94sEIosAZZIznX+dBAKG/8DFm0I='
-
- self.cli_set(base_path + [interface, 'address', '172.16.0.1/24'])
- self.cli_set(base_path + [interface, 'private-key', privkey])
+ for intf in self._interfaces:
+ for option in self._options.get(intf, []):
+ self.cli_set(self._base_path + [intf] + option.split())
- self.cli_set(base_path + [interface, 'peer', 'PEER01', 'port', port])
- self.cli_set(base_path + [interface, 'peer', 'PEER01', 'public-key', pubkey])
- self.cli_set(base_path + [interface, 'peer', 'PEER01', 'allowed-ips', '10.205.212.10/32'])
- self.cli_set(base_path + [interface, 'peer', 'PEER01', 'address', '192.0.2.1'])
- self.cli_set(base_path + [interface, 'per-client-thread'])
+ self.cli_set(base_path + [intf, 'per-client-thread'])
# Commit peers
self.cli_commit()
- tmp = read_file(f'/sys/class/net/{interface}/threaded')
- self.assertTrue(tmp, "1")
- def test_05_wireguard_peer_pubkey_change(self):
+ for intf in self._interfaces:
+ tmp = read_file(f'/sys/class/net/{intf}/threaded')
+ self.assertTrue(tmp, "1")
+
+ def test_wireguard_peer_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
@@ -169,25 +174,27 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.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,9 +203,112 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.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'
+ port = '54321'
+ privkey = 'UOWIeZKNzijhgu0bPRy2PB3gnuOBLfQax5GiYfkmU3A='
+ pubkey = '4nG5NfhHBQUq/DnwT0RjRoBCqAh3VrRHqdQgzC/xujk='
+
+ base_interface_path = base_path + [interface]
+ self.cli_set(base_interface_path + ['address', '172.16.0.1/24'])
+ self.cli_set(base_interface_path + ['private-key', privkey])
+
+ peer_base_path = base_interface_path + ['peer', 'dynamic01']
+ self.cli_set(peer_base_path + ['port', port])
+ self.cli_set(peer_base_path + ['public-key', pubkey])
+ self.cli_set(peer_base_path + ['allowed-ips', '169.254.0.0/16'])
+ self.cli_set(peer_base_path + ['address', '192.0.2.1'])
+ self.cli_set(peer_base_path + ['host-name', 'wg.vyos.net'])
+
+ # Peer address and host-name are mutually exclusive
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(peer_base_path + ['address'])
+
+ # Commit peers
+ self.cli_commit()
+
+ # Ensure the service is running which checks for DNS changes
+ self.assertTrue(is_systemd_service_running(domain_resolver))
+
+ self.cli_delete(base_interface_path)
+ self.cli_commit()
+
+ # Ensure the service is no longer running after WireGuard interface is deleted
+ self.assertFalse(is_systemd_service_running(domain_resolver))
+
+ 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_reverse-proxy.py b/smoketest/scripts/cli/test_load-balancing_haproxy.py
index 34f77b95d..9cb031276 100755
--- a/smoketest/scripts/cli/test_load-balancing_reverse-proxy.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,17 +14,22 @@
# 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
PROCESS_NAME = 'haproxy'
HAPROXY_CONF = '/run/haproxy/haproxy.cfg'
-base_path = ['load-balancing', 'reverse-proxy']
+base_path = ['load-balancing', 'haproxy']
proxy_interface = 'eth1'
valid_ca_cert = """
@@ -131,7 +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,5 +590,144 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.assertIn('log /dev/log local5 notice', config)
self.assertIn('log /dev/log local6 crit', config)
+ def test_reverse_proxy_http_compression(self):
+ # Setup base
+ self.configure_pki()
+ self.base_config()
+
+ # Configure compression in frontend
+ http_comp_path = base_path + ['service', haproxy_service_name, 'http-compression']
+ self.cli_set(http_comp_path + ['algorithm', 'gzip'])
+ self.cli_set(http_comp_path + ['mime-type', 'text/html'])
+ self.cli_set(http_comp_path + ['mime-type', 'text/javascript'])
+ self.cli_set(http_comp_path + ['mime-type', 'text/plain'])
+ self.cli_commit()
+
+ # Test compression is present in generated configuration file
+ config = read_file(HAPROXY_CONF)
+ self.assertIn('filter compression', config)
+ self.assertIn('compression algo gzip', config)
+ self.assertIn('compression type text/html text/javascript text/plain', config)
+
+ # Test setting compression without specifying any mime-types fails verification
+ self.cli_delete(base_path + ['service', haproxy_service_name, 'http-compression', 'mime-type'])
+ with self.assertRaises(ConfigSessionError) as e:
+ self.cli_commit()
+
+ def test_reverse_proxy_timeout(self):
+ t_default_check = '5'
+ t_default_client = '50'
+ t_default_connect = '10'
+ t_default_server ='50'
+ t_check = '4'
+ t_client = '300'
+ t_connect = '12'
+ t_server ='120'
+ t_front_client = '600'
+
+ self.base_config()
+ self.cli_commit()
+ # Check default timeout options
+ config_entries = (
+ f'timeout check {t_default_check}s',
+ f'timeout connect {t_default_connect}s',
+ f'timeout client {t_default_client}s',
+ f'timeout server {t_default_server}s',
+ )
+ # Check default timeout options
+ config = read_file(HAPROXY_CONF)
+ for config_entry in config_entries:
+ self.assertIn(config_entry, config)
+
+ # Set custom timeout options
+ self.cli_set(base_path + ['timeout', 'check', t_check])
+ self.cli_set(base_path + ['timeout', 'client', t_client])
+ self.cli_set(base_path + ['timeout', 'connect', t_connect])
+ self.cli_set(base_path + ['timeout', 'server', t_server])
+ self.cli_set(base_path + ['service', haproxy_service_name, 'timeout', 'client', t_front_client])
+
+ self.cli_commit()
+
+ # Check custom timeout options
+ config_entries = (
+ f'timeout check {t_check}s',
+ f'timeout connect {t_connect}s',
+ f'timeout client {t_client}s',
+ f'timeout server {t_server}s',
+ f'timeout client {t_front_client}s',
+ )
+
+ # Check configured options
+ config = read_file(HAPROXY_CONF)
+ for config_entry in config_entries:
+ self.assertIn(config_entry, config)
+
+ def test_reverse_proxy_http_redirect(self):
+ self.base_config()
+ self.cli_set(base_path + ['service', haproxy_service_name, 'redirect-http-to-https'])
+
+ self.cli_commit()
+
+ config = parse_haproxy_config()
+ frontend_name = f'frontend {haproxy_service_name}-http'
+ self.assertIn(frontend_name, config.keys())
+ self.assertIn('mode http', config[frontend_name])
+ self.assertIn('bind [::]:80 v4v6', config[frontend_name])
+ self.assertIn('acl acme_acl path_beg /.well-known/acme-challenge/', config[frontend_name])
+ self.assertIn('use_backend buildin_acme_certbot if acme_acl', config[frontend_name])
+ self.assertIn('redirect scheme https code 301 if !acme_acl', config[frontend_name])
+
+ backend_name = 'backend buildin_acme_certbot'
+ self.assertIn(backend_name, config.keys())
+ port = get_default_port('certbot_haproxy')
+ self.assertIn(f'server localhost 127.0.0.1:{port}', config[backend_name])
+
+ 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 92b4000b8..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-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,12 +14,17 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import os
import unittest
import time
from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.utils.file import chmod_755
+from vyos.utils.file import write_file
+from vyos.utils.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']
@@ -54,6 +59,19 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase):
self.cli_delete(base_path)
self.cli_commit()
+ removed_chains = [
+ 'wlb_mangle_isp_veth1',
+ 'wlb_mangle_isp_veth2',
+ 'wlb_mangle_isp_eth201',
+ 'wlb_mangle_isp_eth202'
+ ]
+
+ for chain in removed_chains:
+ self.verify_nftables_chain_exists('ip vyos_wanloadbalance', chain, inverse=True)
+
+ # always forward to base class
+ super().tearDown()
+
def test_table_routes(self):
ns1 = 'ns201'
ns2 = 'ns202'
@@ -93,6 +111,7 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase):
cmd_in_netns(ns3, 'ip link set dev eth0 up')
# Set load-balancing configuration
+ self.cli_set(base_path + ['wan', 'hook', '/bin/true'])
self.cli_set(base_path + ['wan', 'interface-health', iface1, 'failure-count', '2'])
self.cli_set(base_path + ['wan', 'interface-health', iface1, 'nexthop', '203.0.113.1'])
self.cli_set(base_path + ['wan', 'interface-health', iface1, 'success-count', '1'])
@@ -102,7 +121,8 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase):
self.cli_set(base_path + ['wan', 'rule', '10', 'inbound-interface', iface3])
self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'address', '198.51.100.0/24'])
-
+ self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface1])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface2])
# commit changes
self.cli_commit()
@@ -118,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}')
@@ -127,7 +164,6 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase):
delete_netns(ns3)
def test_check_chains(self):
-
ns1 = 'nsA'
ns2 = 'nsB'
ns3 = 'nsC'
@@ -137,43 +173,28 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase):
container_iface1 = 'ceth0'
container_iface2 = 'ceth1'
container_iface3 = 'ceth2'
- mangle_isp1 = """table ip mangle {
- chain ISP_veth1 {
- counter ct mark set 0xc9
- counter meta mark set 0xc9
- counter accept
+ mangle_isp1 = """table ip vyos_wanloadbalance {
+ chain wlb_mangle_isp_veth1 {
+ meta mark set 0x000000c9 ct mark set 0x000000c9 counter accept
}
}"""
- mangle_isp2 = """table ip mangle {
- chain ISP_veth2 {
- counter ct mark set 0xca
- counter meta mark set 0xca
- counter accept
+ mangle_isp2 = """table ip vyos_wanloadbalance {
+ chain wlb_mangle_isp_veth2 {
+ meta mark set 0x000000ca ct mark set 0x000000ca counter accept
}
}"""
- mangle_prerouting = """table ip mangle {
- chain PREROUTING {
+ mangle_prerouting = """table ip vyos_wanloadbalance {
+ chain wlb_mangle_prerouting {
type filter hook prerouting priority mangle; policy accept;
- counter jump WANLOADBALANCE_PRE
- }
-}"""
- mangle_wanloadbalance_pre = """table ip mangle {
- chain WANLOADBALANCE_PRE {
- iifname "veth3" ip saddr 198.51.100.0/24 ct state new meta random & 2147483647 < 1073741824 counter jump ISP_veth1
- iifname "veth3" ip saddr 198.51.100.0/24 ct state new counter jump ISP_veth2
+ iifname "veth3" ip saddr 198.51.100.0/24 ct state new counter numgen random mod 11 vmap { 0 : jump wlb_mangle_isp_veth1, 1-10 : jump wlb_mangle_isp_veth2 }
iifname "veth3" ip saddr 198.51.100.0/24 counter meta mark set ct mark
}
}"""
- nat_wanloadbalance = """table ip nat {
- chain WANLOADBALANCE {
- ct mark 0xc9 counter snat to 203.0.113.10
- ct mark 0xca counter snat to 192.0.2.10
- }
-}"""
- nat_vyos_pre_snat_hook = """table ip nat {
- chain VYOS_PRE_SNAT_HOOK {
+ nat_wanloadbalance = """table ip vyos_wanloadbalance {
+ chain wlb_nat_postrouting {
type nat hook postrouting priority srcnat - 1; policy accept;
- counter jump WANLOADBALANCE
+ ct mark 0x000000c9 oifname "veth1" counter snat to 203.0.113.10
+ ct mark 0x000000ca oifname "veth2" counter snat to 192.0.2.10
}
}"""
@@ -214,7 +235,7 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase):
self.cli_set(base_path + ['wan', 'rule', '10', 'inbound-interface', iface3])
self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'address', '198.51.100.0/24'])
self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface1])
- self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface2])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface2, 'weight', '10'])
# commit changes
self.cli_commit()
@@ -222,24 +243,39 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase):
time.sleep(5)
# Check mangle chains
- tmp = cmd(f'sudo nft -s list chain mangle ISP_{iface1}')
+ tmp = cmd(f'sudo nft -s list chain ip vyos_wanloadbalance wlb_mangle_isp_{iface1}')
self.assertEqual(tmp, mangle_isp1)
- tmp = cmd(f'sudo nft -s list chain mangle ISP_{iface2}')
+ tmp = cmd(f'sudo nft -s list chain ip vyos_wanloadbalance wlb_mangle_isp_{iface2}')
self.assertEqual(tmp, mangle_isp2)
- tmp = cmd(f'sudo nft -s list chain mangle PREROUTING')
+ tmp = cmd('sudo nft -s list chain ip vyos_wanloadbalance wlb_mangle_prerouting')
self.assertEqual(tmp, mangle_prerouting)
- tmp = cmd(f'sudo nft -s list chain mangle WANLOADBALANCE_PRE')
- self.assertEqual(tmp, mangle_wanloadbalance_pre)
-
# Check nat chains
- tmp = cmd(f'sudo nft -s list chain nat WANLOADBALANCE')
+ tmp = cmd('sudo nft -s list chain ip vyos_wanloadbalance wlb_nat_postrouting')
self.assertEqual(tmp, nat_wanloadbalance)
- tmp = cmd(f'sudo nft -s list chain nat VYOS_PRE_SNAT_HOOK')
- self.assertEqual(tmp, nat_vyos_pre_snat_hook)
+ # 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]:
@@ -249,6 +285,239 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase):
delete_netns(ns2)
delete_netns(ns3)
+ def test_criteria_failover_hook(self):
+ isp1_iface = 'eth0'
+ isp2_iface = 'eth1'
+ lan_iface = 'eth2'
+
+ hook_path = '/tmp/wlb_hook.sh'
+ hook_output_path = '/tmp/wlb_hook_output'
+ hook_script = f"""
+#!/bin/sh
+
+ifname=$WLB_INTERFACE_NAME
+state=$WLB_INTERFACE_STATE
+
+echo "$ifname - $state" > {hook_output_path}
+"""
+
+ write_file(hook_path, hook_script)
+ chmod_755(hook_path)
+
+ self.cli_set(['interfaces', 'ethernet', isp1_iface, 'address', '203.0.113.2/30'])
+ self.cli_set(['interfaces', 'ethernet', isp2_iface, 'address', '192.0.2.2/30'])
+ self.cli_set(['interfaces', 'ethernet', lan_iface, 'address', '198.51.100.2/30'])
+
+ self.cli_set(base_path + ['wan', 'hook', hook_path])
+ self.cli_set(base_path + ['wan', 'interface-health', isp1_iface, 'failure-count', '1'])
+ self.cli_set(base_path + ['wan', 'interface-health', isp1_iface, 'nexthop', '203.0.113.2'])
+ self.cli_set(base_path + ['wan', 'interface-health', isp1_iface, 'success-count', '1'])
+ self.cli_set(base_path + ['wan', 'interface-health', isp2_iface, 'failure-count', '1'])
+ self.cli_set(base_path + ['wan', 'interface-health', isp2_iface, 'nexthop', '192.0.2.2'])
+ self.cli_set(base_path + ['wan', 'interface-health', isp2_iface, 'success-count', '1'])
+ self.cli_set(base_path + ['wan', 'rule', '5', 'exclude'])
+ self.cli_set(base_path + ['wan', 'rule', '5', 'inbound-interface', 'eth*'])
+ self.cli_set(base_path + ['wan', 'rule', '5', 'destination', 'address', '10.0.0.0/8'])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'failover'])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'inbound-interface', lan_iface])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'protocol', 'udp'])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'address', '198.51.100.0/24'])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'port', '53'])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'destination', 'address', '192.0.2.0/24'])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'destination', 'port', '53'])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'interface', isp1_iface])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'interface', isp1_iface, 'weight', '10'])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'interface', isp2_iface])
+ 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()
+
+ time.sleep(5)
+
+ # Verify isp1 + criteria
+
+ nftables_search = [
+ [f'iifname "eth*"', 'ip daddr 10.0.0.0/8', 'return'],
+ [
+ f'iifname "{lan_iface}"',
+ 'ip saddr 198.51.100.0/24',
+ 'udp sport 53',
+ 'ip daddr 192.0.2.0/24',
+ 'udp dport 53',
+ f'jump wlb_mangle_isp_{isp1_iface}',
+ ],
+ [
+ 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')
+
+ # Trigger failure on isp1 health check
+
+ self.cli_delete(['interfaces', 'ethernet', isp1_iface, 'address', '203.0.113.2/30'])
+ self.cli_commit()
+
+ time.sleep(10)
+
+ # Verify failover to isp2
+
+ nftables_search = [
+ [f'iifname "{lan_iface}"', f'jump wlb_mangle_isp_{isp2_iface}']
+ ]
+
+ self.verify_nftables_chain(nftables_search, 'ip vyos_wanloadbalance', 'wlb_mangle_prerouting')
+
+ # Verify hook output
+
+ self.assertTrue(os.path.exists(hook_output_path))
+
+ with open(hook_output_path, 'r') as f:
+ self.assertIn('eth0 - FAILED', f.read())
+
+ 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 5161e47fd..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
@@ -84,7 +89,7 @@ class TestNAT(VyOSUnitTestSHIM.TestCase):
address_group = 'smoketest_addr'
address_group_member = '192.0.2.1'
interface_group = 'smoketest_ifaces'
- interface_group_member = 'bond.99'
+ interface_group_member = 'eth0'
self.cli_set(['firewall', 'group', 'address-group', address_group, 'address', address_group_member])
self.cli_set(['firewall', 'group', 'interface-group', interface_group, 'interface', interface_group_member])
@@ -304,5 +309,31 @@ class TestNAT(VyOSUnitTestSHIM.TestCase):
self.verify_nftables(nftables_search, 'ip vyos_nat')
+ def test_nat_fqdn(self):
+ source_domain = 'vyos.dev'
+ destination_domain = 'vyos.io'
+
+ self.cli_set(src_path + ['rule', '1', 'outbound-interface', 'name', 'eth0'])
+ self.cli_set(src_path + ['rule', '1', 'source', 'fqdn', source_domain])
+ self.cli_set(src_path + ['rule', '1', 'translation', 'address', 'masquerade'])
+
+ self.cli_set(dst_path + ['rule', '1', 'destination', 'fqdn', destination_domain])
+ self.cli_set(dst_path + ['rule', '1', 'source', 'fqdn', source_domain])
+ self.cli_set(dst_path + ['rule', '1', 'destination', 'port', '5122'])
+ self.cli_set(dst_path + ['rule', '1', 'protocol', 'tcp'])
+ self.cli_set(dst_path + ['rule', '1', 'translation', 'address', '198.51.100.1'])
+ self.cli_set(dst_path + ['rule', '1', 'translation', 'port', '22'])
+
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['set FQDN_nat_destination_1_d'],
+ ['set FQDN_nat_source_1_s'],
+ ['oifname "eth0"', 'ip saddr @FQDN_nat_source_1_s', 'masquerade', 'comment "SRC-NAT-1"'],
+ ['tcp dport 5122', 'ip saddr @FQDN_nat_destination_1_s', 'ip daddr @FQDN_nat_destination_1_d', 'dnat to 198.51.100.1:22', 'comment "DST-NAT-1"']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_nat')
if __name__ == '__main__':
- unittest.main(verbosity=2)
+ 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 a0c6ab055..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-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
@@ -24,9 +24,20 @@ from vyos.utils.process import cmd
base_path = ['policy']
class TestPolicy(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestPolicy, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+ cls.cli_delete(cls, ['vrf'])
+
def tearDown(self):
self.cli_delete(base_path)
self.cli_commit()
+ # always forward to base class
+ super().tearDown()
def test_access_list(self):
acls = {
@@ -112,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}'
@@ -203,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}'
@@ -301,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
@@ -359,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
@@ -417,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
@@ -482,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
@@ -560,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
@@ -643,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
@@ -694,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": {
@@ -831,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'
@@ -844,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'
@@ -857,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'
@@ -926,6 +988,12 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase):
'tag': tag,
},
},
+ '7' : {
+ 'action' : 'deny',
+ 'match' : {
+ 'rpki-comm-invalid': '',
+ },
+ },
'10' : {
'action' : 'permit',
'match' : {
@@ -934,6 +1002,12 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase):
'rpki-not-found': '',
},
},
+ '12' : {
+ 'action' : 'permit',
+ 'match' : {
+ 'rpki-comm-not-found': '',
+ },
+ },
'15' : {
'action' : 'permit',
'match' : {
@@ -944,6 +1018,12 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase):
'next' : '',
},
},
+ '17' : {
+ 'action' : 'permit',
+ 'match' : {
+ 'rpki-comm-valid': '',
+ },
+ },
'20' : {
'action' : 'permit',
'match' : {
@@ -982,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' : {
@@ -1137,6 +1216,16 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase):
},
},
},
+ 'vrf-match': {
+ 'rule': {
+ '10': {
+ 'action': 'permit',
+ 'match': {
+ 'source-vrf': 'TEST',
+ },
+ },
+ },
+ },
}
self.cli_set(['policy', 'access-list', access_list, 'rule', '10', 'action', 'permit'])
@@ -1240,14 +1329,24 @@ 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']:
+ self.cli_set(path + ['rule', rule, 'match', 'source-vrf', rule_config['match']['source-vrf']])
if 'tag' in rule_config['match']:
self.cli_set(path + ['rule', rule, 'match', 'tag', rule_config['match']['tag']])
@@ -1414,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)
@@ -1426,6 +1528,18 @@ 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)
if 'tag' in rule_config['match']:
tmp = f'match tag {rule_config["match"]["tag"]}'
self.assertIn(tmp, config)
@@ -1945,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='')
+ 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
@@ -1958,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='')
+ 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
@@ -1972,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='')
+ 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:
@@ -1981,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='')
+ config = self.getFRRconfig(f'route-map {route_map} permit {seq}', stop_section='^exit')
self.assertNotIn(f' set', config)
def sort_ip(output):
@@ -1991,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
new file mode 100644
index 000000000..a127f38cd
--- /dev/null
+++ b/smoketest/scripts/cli/test_policy_local-route.py
@@ -0,0 +1,173 @@
+#!/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
+
+interface = 'eth0'
+mark = '100'
+table_id = '101'
+extra_table_id = '102'
+vrf_name = 'LPBRVRF'
+vrf_rt_id = '202'
+
+class TestPolicyLocalRoute(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestPolicyLocalRoute, cls).setUpClass()
+ # Clear out current configuration to allow running this test on a live system
+ cls.cli_delete(cls, ['policy', 'local-route'])
+ cls.cli_delete(cls, ['policy', 'local-route6'])
+
+ cls.cli_set(cls, ['vrf', 'name', vrf_name, 'table', vrf_rt_id])
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, ['vrf', 'name', vrf_name])
+
+ super(TestPolicyLocalRoute, cls).tearDownClass()
+
+ def tearDown(self):
+ self.cli_delete(['policy', 'local-route'])
+ self.cli_delete(['policy', 'local-route6'])
+ self.cli_commit()
+
+ ip_rule_search = [
+ [f'lookup {table_id}']
+ ]
+
+ self.verify_rules(ip_rule_search, inverse=True)
+ self.verify_rules(ip_rule_search, inverse=True, addr_family='inet6')
+ # 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])
+ self.cli_set(['policy', 'local-route', 'rule', '4', 'protocol', 'udp'])
+ self.cli_set(['policy', 'local-route', 'rule', '4', 'fwmark', mark])
+ self.cli_set(['policy', 'local-route', 'rule', '4', 'destination', 'address', '198.51.100.0/24'])
+ self.cli_set(['policy', 'local-route', 'rule', '4', 'destination', 'port', '111'])
+ self.cli_set(['policy', 'local-route', 'rule', '4', 'source', 'address', '198.51.100.1'])
+ self.cli_set(['policy', 'local-route', 'rule', '4', 'source', 'port', '443'])
+ self.cli_set(['policy', 'local-route', 'rule', '4', 'set', 'table', table_id])
+
+ self.cli_set(['policy', 'local-route6', 'rule', '6', 'inbound-interface', interface])
+ self.cli_set(['policy', 'local-route6', 'rule', '6', 'protocol', 'tcp'])
+ self.cli_set(['policy', 'local-route6', 'rule', '6', 'fwmark', mark])
+ self.cli_set(['policy', 'local-route6', 'rule', '6', 'destination', 'address', '2001:db8::/64'])
+ self.cli_set(['policy', 'local-route6', 'rule', '6', 'destination', 'port', '123'])
+ self.cli_set(['policy', 'local-route6', 'rule', '6', 'source', 'address', '2001:db8::1'])
+ self.cli_set(['policy', 'local-route6', 'rule', '6', 'source', 'port', '80'])
+ self.cli_set(['policy', 'local-route6', 'rule', '6', 'set', 'table', table_id])
+
+ self.cli_commit()
+
+ rule_lookup = f'lookup {table_id}'
+ rule_fwmark = 'fwmark ' + hex(int(mark))
+ rule_interface = f'iif {interface}'
+
+ ip4_rule_search = [
+ ['from 198.51.100.1', 'to 198.51.100.0/24', rule_fwmark, rule_interface, 'ipproto udp', 'sport 443', 'dport 111', rule_lookup]
+ ]
+
+ self.verify_rules(ip4_rule_search)
+
+ ip6_rule_search = [
+ ['from 2001:db8::1', 'to 2001:db8::/64', rule_fwmark, rule_interface, 'ipproto tcp', 'sport 80', 'dport 123', rule_lookup]
+ ]
+
+ self.verify_rules(ip6_rule_search, addr_family='inet6')
+
+ def test_local_pbr_rule_removal(self):
+ self.cli_set(['policy', 'local-route', 'rule', '1', 'destination', 'address', '198.51.100.1'])
+ self.cli_set(['policy', 'local-route', 'rule', '1', 'set', 'table', table_id])
+
+ self.cli_set(['policy', 'local-route', 'rule', '2', 'destination', 'address', '198.51.100.2'])
+ self.cli_set(['policy', 'local-route', 'rule', '2', 'set', 'table', table_id])
+
+ self.cli_set(['policy', 'local-route', 'rule', '3', 'destination', 'address', '198.51.100.3'])
+ self.cli_set(['policy', 'local-route', 'rule', '3', 'set', 'table', table_id])
+
+ self.cli_commit()
+
+ rule_lookup = f'lookup {table_id}'
+
+ ip_rule_search = [
+ ['to 198.51.100.1', rule_lookup],
+ ['to 198.51.100.2', rule_lookup],
+ ['to 198.51.100.3', rule_lookup],
+ ]
+
+ self.verify_rules(ip_rule_search)
+
+ self.cli_delete(['policy', 'local-route', 'rule', '2'])
+ self.cli_commit()
+
+ ip_rule_missing = [
+ ['to 198.51.100.2', rule_lookup],
+ ]
+
+ self.verify_rules(ip_rule_missing, inverse=True)
+
+ def test_local_pbr_rule_changes(self):
+ self.cli_set(['policy', 'local-route', 'rule', '1', 'destination', 'address', '198.51.100.0/24'])
+ self.cli_set(['policy', 'local-route', 'rule', '1', 'set', 'table', table_id])
+
+ self.cli_commit()
+
+ self.cli_set(['policy', 'local-route', 'rule', '1', 'set', 'table', extra_table_id])
+ self.cli_commit()
+
+ ip_rule_search_extra = [
+ ['to 198.51.100.0/24', f'lookup {extra_table_id}']
+ ]
+
+ self.verify_rules(ip_rule_search_extra)
+
+ ip_rule_search_orig = [
+ ['to 198.51.100.0/24', f'lookup {table_id}']
+ ]
+
+ self.verify_rules(ip_rule_search_orig, inverse=True)
+
+ self.cli_delete(['policy', 'local-route', 'rule', '1', 'set', 'table'])
+ self.cli_set(['policy', 'local-route', 'rule', '1', 'set', 'vrf', vrf_name])
+
+ self.cli_commit()
+
+ ip_rule_search_vrf = [
+ ['to 198.51.100.0/24', f'lookup {vrf_name}']
+ ]
+
+ self.verify_rules(ip_rule_search_extra, inverse=True)
+ self.verify_rules(ip_rule_search_vrf)
+
+ def test_local_pbr_target_vrf(self):
+ self.cli_set(['policy', 'local-route', 'rule', '1', 'destination', 'address', '198.51.100.0/24'])
+ self.cli_set(['policy', 'local-route', 'rule', '1', 'set', 'vrf', vrf_name])
+
+ self.cli_commit()
+
+ ip_rule_search = [
+ ['to 198.51.100.0/24', f'lookup {vrf_name}']
+ ]
+
+ self.verify_rules(ip_rule_search)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on())
diff --git a/smoketest/scripts/cli/test_policy_route.py b/smoketest/scripts/cli/test_policy_route.py
index 797ab9770..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-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,8 +18,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.utils.process import cmd
-
mark = '100'
conn_mark = '555'
conn_mark_set = '111'
@@ -41,7 +39,7 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase):
cls.cli_set(cls, ['interfaces', 'ethernet', interface, 'address', interface_ip])
cls.cli_set(cls, ['protocols', 'static', 'table', table_id, 'route', '0.0.0.0/0', 'interface', interface])
-
+
cls.cli_set(cls, ['vrf', 'name', vrf, 'table', vrf_table_id])
@classmethod
@@ -72,17 +70,8 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase):
]
self.verify_rules(ip_rule_search, inverse=True)
-
- def verify_rules(self, rules_search, inverse=False):
- rule_output = cmd('ip rule show')
-
- for search in rules_search:
- matched = False
- for line in rule_output.split("\n"):
- if all(item in line for item in search):
- matched = True
- break
- self.assertTrue(not matched if inverse else matched, msg=search)
+ # 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'])
@@ -317,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
new file mode 100755
index 000000000..e8e9e6d21
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_babel.py
@@ -0,0 +1,221 @@
+#!/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.ifconfig import Section
+from vyos.frrender import babel_daemon
+from vyos.utils.process import process_named_running
+from vyos.xml_ref import default_value
+
+base_path = ['protocols', 'babel']
+
+class TestProtocolsBABEL(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._interfaces = Section.interfaces('ethernet', vlan=False)
+ # call base-classes classmethod
+ super(TestProtocolsBABEL, cls).setUpClass()
+ # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
+ cls.daemon_pid = process_named_running(babel_daemon)
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+ cls.cli_delete(cls, ['policy', 'prefix-list'])
+ cls.cli_delete(cls, ['policy', 'prefix-list6'])
+
+ def tearDown(self):
+ # always destroy the entire babel configuration to make the processes
+ # life as hard as possible
+ self.cli_delete(base_path)
+ self.cli_delete(['policy', 'prefix-list'])
+ self.cli_delete(['policy', 'prefix-list6'])
+ self.cli_commit()
+
+ # check process health and continuity
+ self.assertEqual(self.daemon_pid, process_named_running(babel_daemon))
+ # always forward to base class
+ super().tearDown()
+
+ def test_01_basic(self):
+ diversity_factor = '64'
+ resend_delay = '100'
+ smoothing_half_life = '400'
+
+ self.cli_set(base_path + ['parameters', 'diversity'])
+ self.cli_set(base_path + ['parameters', 'diversity-factor', diversity_factor])
+ self.cli_set(base_path + ['parameters', 'resend-delay', resend_delay])
+ self.cli_set(base_path + ['parameters', 'smoothing-half-life', smoothing_half_life])
+
+ self.cli_commit()
+
+ frrconfig = self.getFRRconfig('router babel', 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)
+ self.assertIn(f' babel smoothing-half-life {smoothing_half_life}', frrconfig)
+
+ def test_02_redistribute(self):
+ ipv4_protos = ['bgp', 'connected', 'isis', 'kernel', 'nhrp', 'ospf', 'rip', 'static']
+ ipv6_protos = ['bgp', 'connected', 'isis', 'kernel', 'ospfv3', 'ripng', 'static']
+
+ self.cli_set(base_path + ['interface', self._interfaces[0], 'enable-timestamps'])
+
+ for protocol in ipv4_protos:
+ self.cli_set(base_path + ['redistribute', 'ipv4', protocol])
+ for protocol in ipv6_protos:
+ self.cli_set(base_path + ['redistribute', 'ipv6', protocol])
+
+ self.cli_commit()
+
+ frrconfig = self.getFRRconfig('router babel', stop_section='^exit')
+ for protocol in ipv4_protos:
+ self.assertIn(f' redistribute ipv4 {protocol}', frrconfig)
+ for protocol in ipv6_protos:
+ if protocol == 'ospfv3':
+ protocol = 'ospf6'
+ self.assertIn(f' redistribute ipv6 {protocol}', frrconfig)
+
+ def test_03_distribute_list(self):
+ access_list_in4 = '40'
+ access_list_out4 = '50'
+ access_list_in4_iface = '44'
+ access_list_out4_iface = '55'
+ access_list_in6 = 'AL-foo-in6'
+ access_list_out6 = 'AL-foo-out6'
+
+ prefix_list_in4 = 'PL-foo-in4'
+ prefix_list_out4 = 'PL-foo-out4'
+ prefix_list_in6 = 'PL-foo-in6'
+ prefix_list_out6 = 'PL-foo-out6'
+
+ self.cli_set(['policy', 'access-list', access_list_in4])
+ self.cli_set(['policy', 'access-list', access_list_out4])
+ self.cli_set(['policy', 'access-list6', access_list_in6])
+ self.cli_set(['policy', 'access-list6', access_list_out6])
+
+ self.cli_set(['policy', 'access-list', f'{access_list_in4_iface}'])
+ self.cli_set(['policy', 'access-list', f'{access_list_out4_iface}'])
+
+ self.cli_set(['policy', 'prefix-list', prefix_list_in4])
+ self.cli_set(['policy', 'prefix-list', prefix_list_out4])
+ self.cli_set(['policy', 'prefix-list6', prefix_list_in6])
+ self.cli_set(['policy', 'prefix-list6', prefix_list_out6])
+
+ self.cli_set(base_path + ['distribute-list', 'ipv4', 'access-list', 'in', access_list_in4])
+ self.cli_set(base_path + ['distribute-list', 'ipv4', 'access-list', 'out', access_list_out4])
+ self.cli_set(base_path + ['distribute-list', 'ipv6', 'access-list', 'in', access_list_in6])
+ self.cli_set(base_path + ['distribute-list', 'ipv6', 'access-list', 'out', access_list_out6])
+
+ self.cli_set(base_path + ['distribute-list', 'ipv4', 'prefix-list', 'in', prefix_list_in4])
+ self.cli_set(base_path + ['distribute-list', 'ipv4', 'prefix-list', 'out', prefix_list_out4])
+ self.cli_set(base_path + ['distribute-list', 'ipv6', 'prefix-list', 'in', prefix_list_in6])
+ self.cli_set(base_path + ['distribute-list', 'ipv6', 'prefix-list', 'out', prefix_list_out6])
+
+ for interface in self._interfaces:
+ self.cli_set(base_path + ['interface', interface])
+
+ self.cli_set(['policy', 'access-list6', f'{access_list_in6}-{interface}'])
+ self.cli_set(['policy', 'access-list6', f'{access_list_out6}-{interface}'])
+
+ self.cli_set(['policy', 'prefix-list', f'{prefix_list_in4}-{interface}'])
+ self.cli_set(['policy', 'prefix-list', f'{prefix_list_out4}-{interface}'])
+ self.cli_set(['policy', 'prefix-list6', f'{prefix_list_in6}-{interface}'])
+ self.cli_set(['policy', 'prefix-list6', f'{prefix_list_out6}-{interface}'])
+
+ tmp_path = base_path + ['distribute-list', 'ipv4', 'interface', interface]
+ self.cli_set(tmp_path + ['access-list', 'in', f'{access_list_in4_iface}'])
+ self.cli_set(tmp_path + ['access-list', 'out', f'{access_list_out4_iface}'])
+ self.cli_set(tmp_path + ['prefix-list', 'in', f'{prefix_list_in4}-{interface}'])
+ self.cli_set(tmp_path + ['prefix-list', 'out', f'{prefix_list_out4}-{interface}'])
+
+ tmp_path = base_path + ['distribute-list', 'ipv6', 'interface', interface]
+ self.cli_set(tmp_path + ['access-list', 'in', f'{access_list_in6}-{interface}'])
+ self.cli_set(tmp_path + ['access-list', 'out', f'{access_list_out6}-{interface}'])
+ self.cli_set(tmp_path + ['prefix-list', 'in', f'{prefix_list_in6}-{interface}'])
+ self.cli_set(tmp_path + ['prefix-list', 'out', f'{prefix_list_out6}-{interface}'])
+
+ self.cli_commit()
+
+ frrconfig = self.getFRRconfig('router babel', 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)
+ self.assertIn(f' ipv6 distribute-list {access_list_out6} out', frrconfig)
+
+ self.assertIn(f' distribute-list prefix {prefix_list_in4} in', frrconfig)
+ self.assertIn(f' distribute-list prefix {prefix_list_out4} out', frrconfig)
+ self.assertIn(f' ipv6 distribute-list prefix {prefix_list_in6} in', frrconfig)
+ self.assertIn(f' ipv6 distribute-list prefix {prefix_list_out6} out', frrconfig)
+
+ for interface in self._interfaces:
+ self.assertIn(f' distribute-list {access_list_in4_iface} in {interface}', frrconfig)
+ self.assertIn(f' distribute-list {access_list_out4_iface} out {interface}', frrconfig)
+ self.assertIn(f' ipv6 distribute-list {access_list_in6}-{interface} in {interface}', frrconfig)
+ self.assertIn(f' ipv6 distribute-list {access_list_out6}-{interface} out {interface}', frrconfig)
+
+ self.assertIn(f' distribute-list prefix {prefix_list_in4}-{interface} in {interface}', frrconfig)
+ self.assertIn(f' distribute-list prefix {prefix_list_out4}-{interface} out {interface}', frrconfig)
+ self.assertIn(f' ipv6 distribute-list prefix {prefix_list_in6}-{interface} in {interface}', frrconfig)
+ self.assertIn(f' ipv6 distribute-list prefix {prefix_list_out6}-{interface} out {interface}', frrconfig)
+
+ def test_04_interfaces(self):
+ def_update_interval = default_value(base_path + ['interface', 'eth0', 'update-interval'])
+ channel = '20'
+ hello_interval = '1000'
+ max_rtt_penalty = '100'
+ rtt_decay = '23'
+ rtt_max = '119'
+ rtt_min = '11'
+ rxcost = '40000'
+ type = 'wired'
+
+ for interface in self._interfaces:
+ self.cli_set(base_path + ['interface', interface])
+ self.cli_set(base_path + ['interface', interface, 'channel', channel])
+ self.cli_set(base_path + ['interface', interface, 'enable-timestamps'])
+ self.cli_set(base_path + ['interface', interface, 'hello-interval', hello_interval])
+ self.cli_set(base_path + ['interface', interface, 'max-rtt-penalty', max_rtt_penalty])
+ self.cli_set(base_path + ['interface', interface, 'rtt-decay', rtt_decay])
+ self.cli_set(base_path + ['interface', interface, 'rtt-max', rtt_max])
+ self.cli_set(base_path + ['interface', interface, 'rtt-min', rtt_min])
+ self.cli_set(base_path + ['interface', interface, 'rxcost', rxcost])
+ self.cli_set(base_path + ['interface', interface, 'split-horizon', 'disable'])
+ self.cli_set(base_path + ['interface', interface, 'type', type])
+
+ self.cli_commit()
+
+ frrconfig = self.getFRRconfig('router babel', stop_section='^exit')
+ for interface in self._interfaces:
+ self.assertIn(f' network {interface}', frrconfig)
+
+ 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)
+ self.assertIn(f' babel hello-interval {hello_interval}', iface_config)
+ self.assertIn(f' babel rtt-decay {rtt_decay}', iface_config)
+ self.assertIn(f' babel rtt-max {rtt_max}', iface_config)
+ self.assertIn(f' babel rtt-min {rtt_min}', iface_config)
+ self.assertIn(f' babel rxcost {rxcost}', iface_config)
+ self.assertIn(f' babel max-rtt-penalty {max_rtt_penalty}', iface_config)
+ self.assertIn(f' no babel split-horizon', iface_config)
+ self.assertIn(f' babel {type}', iface_config)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on())
diff --git a/smoketest/scripts/cli/test_protocols_bfd.py b/smoketest/scripts/cli/test_protocols_bfd.py
index 716d0a806..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-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,10 +17,11 @@
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
+
from vyos.configsession import ConfigSessionError
+from vyos.frrender import bfd_daemon
from vyos.utils.process import process_named_running
-PROCESS_NAME = 'bfdd'
base_path = ['protocols', 'bfd']
dum_if = 'dum1001'
@@ -84,7 +85,7 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase):
super(TestProtocolsBFD, cls).setUpClass()
# Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
- cls.daemon_pid = process_named_running(PROCESS_NAME)
+ cls.daemon_pid = process_named_running(bfd_daemon)
# ensure we can also run this test on a live system - so lets clean
# out the current configuration :)
@@ -95,7 +96,9 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# check process health and continuity
- self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME))
+ self.assertEqual(self.daemon_pid, process_named_running(bfd_daemon))
+ # always forward to base class
+ super().tearDown()
def test_bfd_peer(self):
self.cli_set(['vrf', 'name', vrf_name, 'table', '1000'])
@@ -130,7 +133,7 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR bgpd configuration
- frrconfig = self.getFRRconfig('bfd', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('bfd', stop_section='^exit')
for peer, peer_config in peers.items():
tmp = f'peer {peer}'
if 'multihop' in peer_config:
@@ -143,8 +146,9 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase):
tmp += f' vrf {peer_config["vrf"]}'
self.assertIn(tmp, frrconfig)
- peerconfig = self.getFRRconfig(f' peer {peer}', end='', daemon=PROCESS_NAME)
-
+ peerconfig = self.getFRRconfig('bfd', 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:
@@ -206,7 +210,8 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase):
# Verify FRR bgpd configuration
for profile, profile_config in profiles.items():
- config = self.getFRRconfig(f' profile {profile}', endsection='^ !')
+ config = self.getFRRconfig('bfd', 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:
@@ -228,9 +233,10 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase):
self.assertNotIn(f'shutdown', config)
for peer, peer_config in peers.items():
- peerconfig = self.getFRRconfig(f' peer {peer}', end='', daemon=PROCESS_NAME)
+ peerconfig = self.getFRRconfig('bfd', 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 ea2f561a4..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-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
@@ -25,8 +25,8 @@ from vyos.configsession import ConfigSessionError
from vyos.template import is_ipv6
from vyos.utils.process import process_named_running
from vyos.utils.process import cmd
+from vyos.frrender import bgp_daemon
-PROCESS_NAME = 'bgpd'
ASN = '64512'
base_path = ['protocols', 'bgp']
@@ -40,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' : {
@@ -64,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',
@@ -111,7 +111,7 @@ neighbor_config = {
'local_role_strict': '',
},
'2001:db8::2' : {
- 'remote_as' : '456',
+ 'remote_as' : 'auto',
'shutdown' : '',
'no_cap_nego' : '',
'port' : '667',
@@ -134,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',
@@ -178,7 +179,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
super(TestProtocolsBGP, cls).setUpClass()
# Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
- cls.daemon_pid = process_named_running(PROCESS_NAME)
+ cls.daemon_pid = process_named_running(bgp_daemon)
# ensure we can also run this test on a live system - so lets clean
# out the current configuration :)
@@ -206,8 +207,12 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
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
@@ -217,8 +222,13 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.cli_delete(base_path)
self.cli_commit()
+ 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(PROCESS_NAME))
+ 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'
@@ -319,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'])
@@ -351,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])
@@ -365,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}')
+ 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)
@@ -387,26 +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(' address-family ipv4 unicast')
+ 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(' address-family ipv4 labeled-unicast')
+ 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(' address-family ipv6 unicast')
+ 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!
@@ -510,7 +554,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR bgpd configuration
- frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit')
self.assertIn(f'router bgp {ASN}', frrconfig)
for peer, peer_config in neighbor_config.items():
@@ -557,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:
@@ -615,7 +661,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR bgpd configuration
- frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit')
self.assertIn(f'router bgp {ASN}', frrconfig)
for peer, peer_config in peer_group_config.items():
@@ -642,10 +688,71 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
}
# We want to redistribute ...
- redistributes = ['connected', 'isis', 'kernel', 'ospf', 'rip', 'static']
- for redistribute in redistributes:
- self.cli_set(base_path + ['address-family', 'ipv4-unicast',
- 'redistribute', redistribute])
+ redistributes = {
+ 'babel' : {
+ 'metric' : '100',
+ 'route_map' : 'redistr-ipv4-babel',
+ },
+ 'connected' : {
+ 'metric' : '200',
+ 'route_map' : 'redistr-ipv4-connected',
+ },
+ 'isis' : {
+ 'metric' : '300',
+ 'route_map' : 'redistr-ipv4-isis',
+ },
+ 'kernel' : {
+ 'metric' : '400',
+ 'route_map' : 'redistr-ipv4-kernel',
+ },
+ 'nhrp': {
+ 'metric': '400',
+ 'route_map': 'redistr-ipv4-nhrp',
+ },
+ 'ospf' : {
+ 'metric' : '500',
+ 'route_map' : 'redistr-ipv4-ospf',
+ },
+ 'rip' : {
+ 'metric' : '600',
+ 'route_map' : 'redistr-ipv4-rip',
+ },
+ 'static' : {
+ 'metric' : '700',
+ 'route_map' : 'redistr-ipv4-static',
+ },
+ 'table' : {
+ '10' : {
+ 'metric' : '810',
+ 'route_map' : 'redistr-ipv4-table-10',
+ },
+ '20' : {
+ 'metric' : '820',
+ 'route_map' : 'redistr-ipv4-table-20',
+ },
+ '30' : {
+ 'metric' : '830',
+ 'route_map' : 'redistr-ipv4-table-30',
+ },
+ },
+ }
+ for proto, proto_config in redistributes.items():
+ proto_path = base_path + ['address-family', 'ipv4-unicast', 'redistribute', proto]
+ if proto == 'table':
+ for table, table_config in proto_config.items():
+ self.cli_set(proto_path + [table])
+ if 'metric' in table_config:
+ self.cli_set(proto_path + [table, 'metric'], value=table_config.get('metric'))
+ if 'route_map' in table_config:
+ 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:
+ self.cli_set(proto_path + ['metric', proto_config['metric']])
+ if 'route_map' in proto_config:
+ self.cli_set(['policy', 'route-map', proto_config['route_map'], 'rule', '10', 'action', 'permit'])
+ self.cli_set(proto_path + ['route-map', proto_config['route_map']])
for network, network_config in networks.items():
self.cli_set(base_path + ['address-family', 'ipv4-unicast',
@@ -664,12 +771,31 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR bgpd configuration
- frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit')
self.assertIn(f'router bgp {ASN}', frrconfig)
- self.assertIn(f' address-family ipv4 unicast', frrconfig)
-
- for redistribute in redistributes:
- self.assertIn(f' redistribute {redistribute}', frrconfig)
+ self.assertIn(' address-family ipv4 unicast', frrconfig)
+
+ for proto, proto_config in redistributes.items():
+ if proto == 'table':
+ for table, table_config in proto_config.items():
+ tmp = f' redistribute table-direct {table}'
+ if 'metric' in proto_config:
+ metric = proto_config['metric']
+ tmp += f' metric {metric}'
+ if 'route_map' in proto_config:
+ route_map = proto_config['route_map']
+ tmp += f' route-map {route_map}'
+ self.assertIn(tmp, frrconfig)
+ else:
+ tmp = f' redistribute {proto}'
+ if 'metric' in proto_config:
+ metric = proto_config['metric']
+ tmp += f' metric {metric}'
+ if 'route_map' in proto_config:
+ route_map = proto_config['route_map']
+ tmp += f' route-map {route_map}'
+
+ self.assertIn(tmp, frrconfig)
for network, network_config in networks.items():
self.assertIn(f' network {network}', frrconfig)
@@ -682,6 +808,10 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
command = f'{command} route-map {network_config["route_map"]}'
self.assertIn(command, frrconfig)
+ for proto, proto_config in redistributes.items():
+ if 'route_map' in proto_config:
+ self.cli_delete(['policy', 'route-map', proto_config['route_map']])
+
def test_bgp_05_afi_ipv6(self):
networks = {
'2001:db8:100::/48' : {
@@ -694,10 +824,67 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
}
# We want to redistribute ...
- redistributes = ['connected', 'kernel', 'ospfv3', 'ripng', 'static']
- for redistribute in redistributes:
- self.cli_set(base_path + ['address-family', 'ipv6-unicast',
- 'redistribute', redistribute])
+ redistributes = {
+ 'babel' : {
+ 'metric' : '100',
+ 'route_map' : 'redistr-ipv6-babel',
+ },
+ 'connected' : {
+ 'metric' : '200',
+ 'route_map' : 'redistr-ipv6-connected',
+ },
+ 'isis' : {
+ 'metric' : '300',
+ 'route_map' : 'redistr-ipv6-isis',
+ },
+ 'kernel' : {
+ 'metric' : '400',
+ 'route_map' : 'redistr-ipv6-kernel',
+ },
+ 'ospfv3' : {
+ 'metric' : '500',
+ 'route_map' : 'redistr-ipv6-ospfv3',
+ },
+ 'ripng' : {
+ 'metric' : '600',
+ 'route_map' : 'redistr-ipv6-ripng',
+ },
+ 'static' : {
+ 'metric' : '700',
+ 'route_map' : 'redistr-ipv6-static',
+ },
+ 'table' : {
+ '110' : {
+ 'metric' : '811',
+ 'route_map' : 'redistr-ipv6-table-110',
+ },
+ '120' : {
+ 'metric' : '821',
+ 'route_map' : 'redistr-ipv6-table-120',
+ },
+ '130' : {
+ 'metric' : '831',
+ 'route_map' : 'redistr-ipv6-table-130',
+ },
+ },
+ }
+ for proto, proto_config in redistributes.items():
+ proto_path = base_path + ['address-family', 'ipv6-unicast', 'redistribute', proto]
+ if proto == 'table':
+ for table, table_config in proto_config.items():
+ self.cli_set(proto_path + [table])
+ if 'metric' in table_config:
+ self.cli_set(proto_path + [table, 'metric'], value=table_config.get('metric'))
+ if 'route_map' in table_config:
+ 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:
+ self.cli_set(proto_path + ['metric', proto_config['metric']])
+ if 'route_map' in proto_config:
+ self.cli_set(['policy', 'route-map', proto_config['route_map'], 'rule', '20', 'action', 'permit'])
+ self.cli_set(proto_path + ['route-map', proto_config['route_map']])
for network, network_config in networks.items():
self.cli_set(base_path + ['address-family', 'ipv6-unicast',
@@ -710,24 +897,47 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR bgpd configuration
- frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit')
self.assertIn(f'router bgp {ASN}', frrconfig)
- self.assertIn(f' address-family ipv6 unicast', frrconfig)
+ self.assertIn(' address-family ipv6 unicast', frrconfig)
# T2100: By default ebgp-requires-policy is disabled to keep VyOS
# 1.3 and 1.2 backwards compatibility
- self.assertIn(f' no bgp ebgp-requires-policy', frrconfig)
-
- for redistribute in redistributes:
- # FRR calls this OSPF6
- if redistribute == 'ospfv3':
- redistribute = 'ospf6'
- self.assertIn(f' redistribute {redistribute}', frrconfig)
+ self.assertIn(' no bgp ebgp-requires-policy', frrconfig)
+
+ for proto, proto_config in redistributes.items():
+ if proto == 'table':
+ for table, table_config in proto_config.items():
+ tmp = f' redistribute table-direct {table}'
+ if 'metric' in proto_config:
+ metric = proto_config['metric']
+ tmp += f' metric {metric}'
+ if 'route_map' in proto_config:
+ route_map = proto_config['route_map']
+ tmp += f' route-map {route_map}'
+ self.assertIn(tmp, frrconfig)
+ else:
+ # FRR calls this OSPF6
+ if proto == 'ospfv3':
+ proto = 'ospf6'
+ tmp = f' redistribute {proto}'
+ if 'metric' in proto_config:
+ metric = proto_config['metric']
+ tmp += f' metric {metric}'
+ if 'route_map' in proto_config:
+ route_map = proto_config['route_map']
+ tmp += f' route-map {route_map}'
+
+ self.assertIn(tmp, frrconfig)
for network, network_config in networks.items():
self.assertIn(f' network {network}', frrconfig)
if 'as_set' in network_config:
self.assertIn(f' aggregate-address {network} summary-only', frrconfig)
+ for proto, proto_config in redistributes.items():
+ if 'route_map' in proto_config:
+ self.cli_delete(['policy', 'route-map', proto_config['route_map']])
+
def test_bgp_06_listen_range(self):
# Implemented via T1875
limit = '64'
@@ -752,7 +962,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR bgpd configuration
- frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}', 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)
@@ -787,7 +997,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR bgpd configuration
- frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}', 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)
@@ -800,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}')
+ 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)
@@ -843,7 +1053,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR distances configuration
- frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit')
self.assertIn(f'router bgp {ASN}', frrconfig)
for family in verify_families:
self.assertIn(f'address-family {family}', frrconfig)
@@ -881,7 +1091,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR bgpd configuration
- frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit')
self.assertIn(f'router bgp {ASN}', frrconfig)
self.assertIn(f' address-family ipv6 unicast', frrconfig)
@@ -889,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}')
+ 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)
@@ -907,7 +1117,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR bgpd configuration
- frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}', 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)
@@ -924,7 +1134,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR bgpd configuration
- frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}', 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)
@@ -956,11 +1166,13 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR bgpd configuration
- frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit')
self.assertIn(f'router bgp {ASN}', frrconfig)
for afi in ['ipv4', 'ipv6']:
- afi_config = self.getFRRconfig(f' address-family {afi} unicast', endsection='exit-address-family', daemon='bgpd')
+ afi_config = self.getFRRconfig(f'router bgp {ASN}', 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)
@@ -1005,7 +1217,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
- frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ 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)
@@ -1030,7 +1242,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
- frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ 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)
@@ -1055,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}')
- frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}')
+ 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)
@@ -1078,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}')
- frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}')
+ 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)
@@ -1092,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}')
- frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}')
+ 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)
@@ -1109,8 +1321,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
'vpn', 'export',
import_rd])
self.cli_commit()
- frrconfig = self.getFRRconfig(f'router bgp {ASN}')
- frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}')
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}', 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)
@@ -1139,7 +1351,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
for interface in interfaces:
- frrconfig = self.getFRRconfig(f'interface {interface}')
+ frrconfig = self.getFRRconfig(f'interface {interface}', stop_section='^exit')
self.assertIn(f'interface {interface}', frrconfig)
self.assertIn(f' mpls bgp forwarding', frrconfig)
@@ -1153,7 +1365,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
for interface in interfaces:
- frrconfig = self.getFRRconfig(f'interface {interface}')
+ 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'])
@@ -1173,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}')
+ 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)
@@ -1188,17 +1400,22 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
- frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ 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(' address-family ipv4 unicast')
+ 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(' address-family ipv6 unicast')
+
+ 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}', afiv4_config)
+ self.assertIn(f' nexthop vpn export {nexthop_ipv6}', afiv6_config)
def test_bgp_25_ipv4_labeled_unicast_peer_group(self):
pg_ipv4 = 'foo4'
@@ -1212,14 +1429,16 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
- frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ 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(' address-family ipv4 labeled-unicast')
+ 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)
@@ -1236,28 +1455,78 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
- frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ 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(' address-family ipv6 labeled-unicast')
+ 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(' address-family l2vpn evpn')
+ 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):
@@ -1294,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}')
+ conf = self.getFRRconfig(f'router bgp {ASN}', stop_section='^exit')
_common_config_check(conf)
# test add internal remote-as to external group
@@ -1309,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}')
+ 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)
@@ -1321,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}')
+ 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)
@@ -1346,11 +1615,111 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
- conf = self.getFRRconfig(f'router bgp {ASN}')
+ 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)
+ def test_bgp_30_import_vrf_routemap(self):
+ router_id = '127.0.0.3'
+ table = '1000'
+ vrf = 'red'
+ vrf_base = ['vrf', 'name', vrf]
+ self.cli_set(vrf_base + ['table', table])
+ self.cli_set(vrf_base + ['protocols', 'bgp', 'system-as', ASN])
+ self.cli_set(
+ vrf_base + ['protocols', 'bgp', 'parameters', 'router-id',
+ router_id])
+
+ self.cli_set(
+ base_path + ['address-family', 'ipv4-unicast', 'import',
+ 'vrf', vrf])
+ self.cli_set(
+ base_path + ['address-family', 'ipv4-unicast', 'route-map',
+ 'vrf', 'import', route_map_in])
+
+ self.cli_commit()
+
+ # Verify FRR bgpd configuration
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}',
+ stop_section='^exit')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f' address-family ipv4 unicast', frrconfig)
+
+ self.assertIn(f' import vrf {vrf}', frrconfig)
+ self.assertIn(f' import vrf route-map {route_map_in}', frrconfig)
+
+ # Verify FRR bgpd configuration
+ frr_vrf_config = self.getFRRconfig(
+ f'router bgp {ASN} vrf {vrf}', 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'
@@ -1379,7 +1748,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
# let the bgpd process recover
sleep(10)
# update daemon PID - this was a planned daemon restart
- self.daemon_pid = process_named_running(PROCESS_NAME)
+ self.daemon_pid = process_named_running(bgp_daemon)
# set bmp config but not set address
self.cli_set(target_path + ['port', target_port])
@@ -1395,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}')
+ 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 769f3dd33..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-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,13 @@
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
+
from vyos.configsession import ConfigSessionError
from vyos.ifconfig import Section
from vyos.utils.process import process_named_running
+from vyos.frrender import isis_daemon
-PROCESS_NAME = 'isisd'
base_path = ['protocols', 'isis']
-
domain = 'VyOS'
net = '49.0001.1921.6800.1002.00'
@@ -34,7 +34,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
# call base-classes classmethod
super(TestProtocolsISIS, cls).setUpClass()
# Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
- cls.daemon_pid = process_named_running(PROCESS_NAME)
+ cls.daemon_pid = process_named_running(isis_daemon)
# ensure we can also run this test on a live system - so lets clean
# out the current configuration :)
cls.cli_delete(cls, base_path)
@@ -49,19 +49,16 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# check process health and continuity
- self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME))
-
- def isis_base_config(self):
- self.cli_set(base_path + ['net', net])
- for interface in self._interfaces:
- self.cli_set(base_path + ['interface', interface])
+ self.assertEqual(self.daemon_pid, process_named_running(isis_daemon))
+ # always forward to base class
+ super().tearDown()
def test_isis_01_redistribute(self):
prefix_list = 'EXPORT-ISIS'
route_map = 'EXPORT-ISIS'
rule = '10'
metric_style = 'transition'
-
+ redistribute = ['babel', 'bgp', 'connected', 'kernel', 'nhrp', 'ospf', 'rip', 'static']
self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', rule, 'action', 'permit'])
self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', rule, 'prefix', '203.0.113.0/24'])
self.cli_set(['policy', 'route-map', route_map, 'rule', rule, 'action', 'permit'])
@@ -73,14 +70,18 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
with self.assertRaises(ConfigSessionError):
self.cli_commit()
- self.isis_base_config()
+ self.cli_set(base_path + ['net', net])
+ for interface in self._interfaces:
+ self.cli_set(base_path + ['interface', interface])
self.cli_set(base_path + ['redistribute', 'ipv4', 'connected'])
# verify() - Redistribute level-1 or level-2 should be specified
with self.assertRaises(ConfigSessionError):
self.cli_commit()
- self.cli_set(base_path + ['redistribute', 'ipv4', 'connected', 'level-2', 'route-map', route_map])
+ for proto in redistribute:
+ self.cli_set(base_path + ['redistribute', 'ipv4', proto, 'level-2', 'route-map', route_map])
+
self.cli_set(base_path + ['metric-style', metric_style])
self.cli_set(base_path + ['log-adjacency-changes'])
@@ -88,14 +89,15 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify all changes
- tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ tmp = self.getFRRconfig(f'router isis {domain}', stop_section='^exit')
self.assertIn(f' net {net}', tmp)
self.assertIn(f' metric-style {metric_style}', tmp)
self.assertIn(f' log-adjacency-changes', tmp)
- self.assertIn(f' redistribute ipv4 connected level-2 route-map {route_map}', tmp)
+ for proto in redistribute:
+ self.assertIn(f' redistribute ipv4 {proto} level-2 route-map {route_map}', tmp)
for interface in self._interfaces:
- tmp = self.getFRRconfig(f'interface {interface}', daemon='isisd')
+ tmp = self.getFRRconfig(f'interface {interface}', stop_section='^exit')
self.assertIn(f' ip router isis {domain}', tmp)
self.assertIn(f' ipv6 router isis {domain}', tmp)
@@ -124,11 +126,11 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR isisd configuration
- tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ tmp = self.getFRRconfig(f'router isis {domain}', 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}', daemon='isisd')
+ 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)
@@ -141,7 +143,10 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
metric = '50'
route_map = 'default-foo-'
- self.isis_base_config()
+ self.cli_set(base_path + ['net', net])
+ for interface in self._interfaces:
+ self.cli_set(base_path + ['interface', interface])
+
for afi in ['ipv4', 'ipv6']:
for level in ['level-1', 'level-2']:
self.cli_set(base_path + ['default-information', 'originate', afi, level, 'always'])
@@ -152,7 +157,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify all changes
- tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ tmp = self.getFRRconfig(f'router isis {domain}', stop_section='^exit')
self.assertIn(f' net {net}', tmp)
for afi in ['ipv4', 'ipv6']:
@@ -160,11 +165,11 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
route_map_name = route_map + level + afi
self.assertIn(f' default-information originate {afi} {level} always route-map {route_map_name} metric {metric}', tmp)
-
def test_isis_05_password(self):
password = 'foo'
+ md5_password = 'secret_md5_hash'
- self.isis_base_config()
+ self.cli_set(base_path + ['net', net])
for interface in self._interfaces:
self.cli_set(base_path + ['interface', interface, 'password', 'plaintext-password', f'{password}-{interface}'])
@@ -187,15 +192,38 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify all changes
- tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ tmp = self.getFRRconfig(f'router isis {domain}', 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}', daemon='isisd')
+ 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'
@@ -235,12 +263,12 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify all changes
- tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ tmp = self.getFRRconfig(f'router isis {domain}', 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}', daemon='isisd')
+ 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)
@@ -252,7 +280,6 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
global_block_high = "399"
local_block_low = "400"
local_block_high = "499"
- interface = 'lo'
maximum_stack_size = '5'
prefix_one = '192.168.0.1/32'
prefix_two = '192.168.0.2/32'
@@ -264,7 +291,9 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
prefix_four_value = '65000'
self.cli_set(base_path + ['net', net])
- self.cli_set(base_path + ['interface', interface])
+ for interface in self._interfaces:
+ self.cli_set(base_path + ['interface', interface])
+
self.cli_set(base_path + ['segment-routing', 'maximum-label-depth', maximum_stack_size])
self.cli_set(base_path + ['segment-routing', 'global-block', 'low-label-value', global_block_low])
self.cli_set(base_path + ['segment-routing', 'global-block', 'high-label-value', global_block_high])
@@ -283,7 +312,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify all changes
- tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ tmp = self.getFRRconfig(f'router isis {domain}', 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)
@@ -305,7 +334,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify main ISIS changes
- tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ tmp = self.getFRRconfig(f'router isis {domain}', 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)
@@ -318,7 +347,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
for interface in self._interfaces:
# Verify interface changes for holddown
- tmp = self.getFRRconfig(f'interface {interface}', daemon='isisd')
+ tmp = self.getFRRconfig(f'interface {interface}', 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)
@@ -332,7 +361,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
for interface in self._interfaces:
# Verify interface changes for disable
- tmp = self.getFRRconfig(f'interface {interface}', daemon='isisd')
+ tmp = self.getFRRconfig(f'interface {interface}', 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)
@@ -355,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}', daemon='isisd')
+ 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'])
@@ -365,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}', daemon='isisd')
+ 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'])
@@ -376,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}', daemon='isisd')
+ 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'])
@@ -388,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}', daemon='isisd')
+ 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'])
@@ -408,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}', daemon='isisd')
+ 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 0c1599f9b..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-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,11 +17,12 @@
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
+
from vyos.configsession import ConfigSessionError
from vyos.ifconfig import Section
+from vyos.frrender import ldpd_daemon
from vyos.utils.process import process_named_running
-PROCESS_NAME = 'ldpd'
base_path = ['protocols', 'mpls', 'ldp']
peers = {
@@ -71,8 +72,7 @@ class TestProtocolsMPLS(VyOSUnitTestSHIM.TestCase):
super(TestProtocolsMPLS, cls).setUpClass()
# Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
- cls.daemon_pid = process_named_running(PROCESS_NAME)
-
+ cls.daemon_pid = process_named_running(ldpd_daemon)
# ensure we can also run this test on a live system - so lets clean
# out the current configuration :)
cls.cli_delete(cls, base_path)
@@ -82,7 +82,9 @@ class TestProtocolsMPLS(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# check process health and continuity
- self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME))
+ self.assertEqual(self.daemon_pid, process_named_running(ldpd_daemon))
+ # always forward to base class
+ super().tearDown()
def test_mpls_basic(self):
router_id = '1.2.3.4'
@@ -106,15 +108,86 @@ class TestProtocolsMPLS(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Validate configuration
- frrconfig = self.getFRRconfig('mpls ldp', daemon=PROCESS_NAME)
+ 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', 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)
+
+ def test_02_mpls_disable_establish_hello(self):
+ router_id = '1.2.3.4'
+ transport_ipv4_addr = '5.6.7.8'
+ transport_ipv6_addr = '2001:db8:1111::1111'
+ interfaces = Section.interfaces('ethernet')
+
+ self.cli_set(base_path + ['router-id', router_id])
+
+ # At least one LDP interface must be configured
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'disable-establish-hello'])
+
+ # LDP transport address missing
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['discovery', 'transport-ipv4-address', transport_ipv4_addr])
+ self.cli_set(base_path + ['discovery', 'transport-ipv6-address', transport_ipv6_addr])
+
+ # Commit changes
+ self.cli_commit()
+
+ # Validate configuration
+ frrconfig = self.getFRRconfig('mpls ldp', stop_section='^exit')
self.assertIn(f'mpls ldp', frrconfig)
self.assertIn(f' router-id {router_id}', frrconfig)
# Validate AFI IPv4
- afiv4_config = self.getFRRconfig(' address-family ipv4', daemon=PROCESS_NAME)
+ afiv4_config = self.getFRRconfig('mpls ldp', 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', 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)
+ self.assertIn(f' disable-establish-hello', afiv6_config)
+
+ # Delete disable-establish-hello
+ for interface in interfaces:
+ self.cli_delete(base_path + ['interface', interface, 'disable-establish-hello'])
+
+ # Commit changes
+ self.cli_commit()
+
+ # Validate AFI IPv4
+ afiv4_config = self.getFRRconfig('mpls ldp', stop_section='^exit',
+ start_subsection=' address-family ipv4',
+ stop_subsection='^ exit-address-family')
+ # Validate AFI IPv6
+ 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)
+ self.assertNotIn(f' disable-establish-hello', afiv4_config)
+ self.assertIn(f' interface {interface}', afiv6_config)
+ self.assertNotIn(f' disable-establish-hello', afiv6_config)
+
if __name__ == '__main__':
- unittest.main(verbosity=2)
+ 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 43ae4abf2..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-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,14 +17,12 @@
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-
-from vyos.firewall import find_nftables_rule
from vyos.utils.process import process_named_running
-from vyos.utils.file import read_file
tunnel_path = ['interfaces', 'tunnel']
nhrp_path = ['protocols', 'nhrp']
vpn_path = ['vpn', 'ipsec']
+PROCESS_NAME = 'nhrpd'
class TestProtocolsNHRP(VyOSUnitTestSHIM.TestCase):
@classmethod
@@ -40,30 +38,44 @@ 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_config(self):
+ def test_01_nhrp_config(self):
tunnel_if = "tun100"
- tunnel_source = "192.0.2.1"
+ tunnel_ip = '172.16.253.134/32'
+ tunnel_source = "192.0.2.134"
tunnel_encapsulation = "gre"
esp_group = "ESP-HUB"
ike_group = "IKE-HUB"
nhrp_secret = "vyos123"
nhrp_profile = "NHRPVPN"
+ nhrp_holdtime = '300'
+ nhs_tunnelip = '172.16.253.1'
+ nhs_nbmaip = '192.0.2.1'
+ map_tunnelip = '172.16.253.135'
+ map_nbmaip = "192.0.2.135"
+ nhrp_networkid = '1'
ipsec_secret = "secret"
-
+ multicat_log_group = '2'
+ redirect_log_group = '1'
# Tunnel
- self.cli_set(tunnel_path + [tunnel_if, "address", "172.16.253.134/29"])
+ self.cli_set(tunnel_path + [tunnel_if, "address", tunnel_ip])
self.cli_set(tunnel_path + [tunnel_if, "encapsulation", tunnel_encapsulation])
self.cli_set(tunnel_path + [tunnel_if, "source-address", tunnel_source])
self.cli_set(tunnel_path + [tunnel_if, "enable-multicast"])
self.cli_set(tunnel_path + [tunnel_if, "parameters", "ip", "key", "1"])
# NHRP
- self.cli_set(nhrp_path + ["tunnel", tunnel_if, "cisco-authentication", nhrp_secret])
- self.cli_set(nhrp_path + ["tunnel", tunnel_if, "holding-time", "300"])
- self.cli_set(nhrp_path + ["tunnel", tunnel_if, "multicast", "dynamic"])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "authentication", nhrp_secret])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "holdtime", nhrp_holdtime])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "multicast", nhs_tunnelip])
self.cli_set(nhrp_path + ["tunnel", tunnel_if, "redirect"])
self.cli_set(nhrp_path + ["tunnel", tunnel_if, "shortcut"])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "registration-no-unique"])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "network-id", nhrp_networkid])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "nhs", "tunnel-ip", nhs_tunnelip, "nbma", nhs_nbmaip])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "map", "tunnel-ip", map_tunnelip, "nbma", map_nbmaip])
# IKE/ESP Groups
self.cli_set(vpn_path + ["esp-group", esp_group, "lifetime", "1800"])
@@ -93,29 +105,40 @@ class TestProtocolsNHRP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
- opennhrp_lines = [
- f'interface {tunnel_if} #hub {nhrp_profile}',
- f'cisco-authentication {nhrp_secret}',
- f'holding-time 300',
- f'shortcut',
- f'multicast dynamic',
- f'redirect'
+ frrconfig = self.getFRRconfig(f'interface {tunnel_if}', 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)
+ self.assertIn(f' ip nhrp map multicast {nhs_tunnelip}', frrconfig)
+ self.assertIn(f' ip nhrp redirect', frrconfig)
+ self.assertIn(f' ip nhrp registration no-unique', frrconfig)
+ self.assertIn(f' ip nhrp shortcut', frrconfig)
+ self.assertIn(f' ip nhrp network-id {nhrp_networkid}', frrconfig)
+ self.assertIn(f' ip nhrp nhs {nhs_tunnelip} nbma {nhs_nbmaip}', frrconfig)
+ self.assertIn(f' ip nhrp map {map_tunnelip} {map_nbmaip}', frrconfig)
+ self.assertIn(f' tunnel protection vici profile dmvpn-{nhrp_profile}-{tunnel_if}-child',
+ frrconfig)
+
+ nftables_search_multicast = [
+ ['chain VYOS_NHRP_MULTICAST_OUTPUT'],
+ ['type filter hook output priority filter + 10; policy accept;'],
+ [f'oifname "{tunnel_if}"', 'ip daddr 224.0.0.0/24', 'counter', f'log group {multicat_log_group}'],
+ [f'oifname "{tunnel_if}"', 'ip daddr 224.0.0.0/24', 'counter', 'drop'],
+ ['chain VYOS_NHRP_MULTICAST_FORWARD'],
+ ['type filter hook output priority filter + 10; policy accept;'],
+ [f'oifname "{tunnel_if}"', 'ip daddr 224.0.0.0/4', 'counter', f'log group {multicat_log_group}'],
+ [f'oifname "{tunnel_if}"', 'ip daddr 224.0.0.0/4', 'counter', 'drop']
]
- tmp_opennhrp_conf = read_file('/run/opennhrp/opennhrp.conf')
-
- for line in opennhrp_lines:
- self.assertIn(line, tmp_opennhrp_conf)
-
- firewall_matches = [
- f'ip protocol {tunnel_encapsulation}',
- f'ip saddr {tunnel_source}',
- f'ip daddr 224.0.0.0/4',
- f'comment "VYOS_NHRP_{tunnel_if}"'
+ nftables_search_redirect = [
+ ['chain VYOS_NHRP_REDIRECT_FORWARD'],
+ ['type filter hook forward priority filter + 10; policy accept;'],
+ [f'iifname "{tunnel_if}" oifname "{tunnel_if}"', 'meter loglimit-0 size 65535 { ip daddr & 255.255.255.0 . ip saddr & 255.255.255.0 timeout 1m limit rate 4/minute burst 1 packets }', 'counter', f'log group {redirect_log_group}']
]
+ self.verify_nftables(nftables_search_multicast, 'ip vyos_nhrp_multicast')
+ self.verify_nftables(nftables_search_redirect, 'ip vyos_nhrp_redirect')
- self.assertTrue(find_nftables_rule('ip vyos_nhrp_filter', 'VYOS_NHRP_OUTPUT', firewall_matches) is not None)
- self.assertTrue(process_named_running('opennhrp'))
+ self.assertTrue(process_named_running(PROCESS_NAME))
if __name__ == '__main__':
- unittest.main(verbosity=2)
+ 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 e37aed456..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,10 +17,11 @@
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
+
from vyos.configsession import ConfigSessionError
from vyos.utils.process import process_named_running
+from vyos.frrender import openfabric_daemon
-PROCESS_NAME = 'fabricd'
base_path = ['protocols', 'openfabric']
domain = 'VyOS'
@@ -36,7 +37,7 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase):
# call base-classes classmethod
super(TestProtocolsOpenFabric, cls).setUpClass()
# Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
- cls.daemon_pid = process_named_running(PROCESS_NAME)
+ cls.daemon_pid = process_named_running(openfabric_daemon)
# ensure we can also run this test on a live system - so lets clean
# out the current configuration :)
cls.cli_delete(cls, base_path)
@@ -46,7 +47,9 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# check process health and continuity
- self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME))
+ self.assertEqual(self.daemon_pid, process_named_running(openfabric_daemon))
+ # always forward to base class
+ super().tearDown()
def openfabric_base_config(self):
self.cli_set(['interfaces', 'dummy', dummy_if])
@@ -75,14 +78,14 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify all changes
- tmp = self.getFRRconfig(f'router openfabric {domain}', daemon='fabricd')
+ tmp = self.getFRRconfig(f'router openfabric {domain}', 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}', daemon='fabricd')
+ 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)
@@ -101,12 +104,12 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR openfabric configuration
- tmp = self.getFRRconfig(f'router openfabric {domain}', daemon='fabricd')
+ tmp = self.getFRRconfig(f'router openfabric {domain}', 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}', daemon='fabricd')
+ 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)
@@ -114,6 +117,7 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase):
def test_openfabric_03_password(self):
password = 'foo'
+ md5_password = 'secret_md5_hash'
self.openfabric_base_config()
@@ -137,13 +141,30 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify all changes
- tmp = self.getFRRconfig(f'router openfabric {domain}', daemon='fabricd')
+ tmp = self.getFRRconfig(f'router openfabric {domain}', stop_section='^exit')
self.assertIn(f' net {net}', tmp)
self.assertIn(f' domain-password clear {password}', tmp)
- tmp = self.getFRRconfig(f'interface {dummy_if}', daemon='fabricd')
+ tmp = self.getFRRconfig(f'interface {dummy_if}', 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'
@@ -165,22 +186,21 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR openfabric configuration
- tmp = self.getFRRconfig(f'router openfabric {domain}', daemon='fabricd')
+ tmp = self.getFRRconfig(f'router openfabric {domain}', stop_section='^exit')
self.assertIn(f'router openfabric {domain}', tmp)
self.assertIn(f' net {net}', tmp)
- tmp = self.getFRRconfig(f'router openfabric {domain_2}', daemon='fabricd')
+ tmp = self.getFRRconfig(f'router openfabric {domain_2}', 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}', daemon='fabricd')
+ 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}', daemon='fabricd')
+ 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 905eaf2e9..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-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,13 +16,15 @@
import unittest
+from time import sleep
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
from vyos.ifconfig import Section
+from vyos.frrender import ospf_daemon
from vyos.utils.process import process_named_running
+from vyos.xml_ref import default_value
-PROCESS_NAME = 'ospfd'
base_path = ['protocols', 'ospf']
route_map = 'foo-bar-baz10'
@@ -34,7 +36,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
super(TestProtocolsOSPF, cls).setUpClass()
# Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
- cls.daemon_pid = process_named_running(PROCESS_NAME)
+ cls.daemon_pid = process_named_running(ospf_daemon)
cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit'])
cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '20', 'action', 'permit'])
@@ -54,8 +56,13 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
self.cli_delete(base_path)
self.cli_commit()
+ 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(PROCESS_NAME))
+ 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
@@ -63,7 +70,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf', 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
@@ -91,7 +98,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf', stop_section='^exit')
self.assertIn(f'router ospf', frrconfig)
self.assertIn(f' compatible rfc1583', frrconfig)
self.assertIn(f' auto-cost reference-bandwidth {bandwidth}', frrconfig)
@@ -107,7 +114,6 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
self.assertNotIn(f' area 10 range 10.0.1.0/24 not-advertise', frrconfig)
self.assertIn(f' area 10 range 10.0.2.0/24 not-advertise', frrconfig)
-
def test_ospf_03_access_list(self):
acl = '100'
seq = '10'
@@ -123,14 +129,13 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf', stop_section='^exit')
self.assertIn(f'router ospf', frrconfig)
self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults
for ptotocol in protocols:
self.assertIn(f' distribute-list {acl} out {ptotocol}', frrconfig) # defaults
self.cli_delete(['policy', 'access-list', acl])
-
def test_ospf_04_default_originate(self):
seq = '100'
metric = '50'
@@ -144,7 +149,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf', 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)
@@ -154,10 +159,9 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf', 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):
global_distance = '128'
intra_area = '100'
@@ -196,7 +200,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf', 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
@@ -219,9 +223,16 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
self.cli_set(base_path + ['distance', 'ospf', 'inter-area', inter_area])
self.cli_commit()
- frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf', 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
+ # We need to wait on_shutdown time, until the OSPF process is removed from the CLI
+ # otherwise the test in tearDown() will fail
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ sleep(int(on_shutdown) + 5) # additional grace period of 5 seconds
def test_ospf_06_neighbor(self):
priority = '10'
@@ -235,7 +246,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf', 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
@@ -243,21 +254,42 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
def test_ospf_07_redistribute(self):
metric = '15'
metric_type = '1'
- redistribute = ['babel', 'bgp', 'connected', 'isis', 'kernel', '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', daemon=PROCESS_NAME)
+ 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']
@@ -268,6 +300,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
retransmit = '5'
transmit = '5'
dead = '40'
+ window_default = default_value(base_path + ['area', area, 'virtual-link', virtual_link, 'retransmit-window'])
self.cli_set(base_path + ['area', area, 'shortcut', shortcut])
self.cli_set(base_path + ['area', area, 'virtual-link', virtual_link, 'hello-interval', hello])
@@ -277,14 +310,19 @@ 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', daemon=PROCESS_NAME)
+ 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} transmit-delay {transmit} dead-interval {dead}', frrconfig)
+ self.assertIn(f' area {area} virtual-link {virtual_link} hello-interval {hello} retransmit-interval {retransmit} retransmit-window {window_default} transmit-delay {transmit} dead-interval {dead}', frrconfig)
for network in networks:
self.assertIn(f' network {network} area {area}', frrconfig)
@@ -313,13 +351,13 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
# commit changes
self.cli_commit()
- frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ 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}')
+ 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)
@@ -337,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}', daemon=PROCESS_NAME)
+ 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)
@@ -358,11 +396,11 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf', stop_section='^exit')
self.assertIn(f'router ospf', frrconfig)
for interface in interfaces:
- config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ config = self.getFRRconfig(f'interface {interface}', stop_section='^exit')
self.assertIn(f'interface {interface}', config)
self.assertIn(f' ip ospf area {area}', config)
@@ -385,17 +423,17 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf', 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}', daemon=PROCESS_NAME)
+ 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}', daemon=PROCESS_NAME)
+ 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)
@@ -405,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}', daemon=PROCESS_NAME)
+ 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)
@@ -431,7 +469,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf', 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)
@@ -464,7 +502,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify all changes
- frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ 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)
@@ -482,7 +520,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify main OSPF changes
- frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf', 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)
@@ -495,7 +533,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
for interface in interfaces:
# Verify interface changes for holddown
- config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ config = self.getFRRconfig(f'interface {interface}', 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)
@@ -509,7 +547,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
for interface in interfaces:
# Verify interface changes for disable
- config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ config = self.getFRRconfig(f'interface {interface}', 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)
@@ -531,7 +569,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf', stop_section='^exit')
self.assertIn(f'router ospf', frrconfig)
self.assertIn(f' capability opaque', frrconfig)
self.assertIn(f' graceful-restart grace-period {period}', frrconfig)
@@ -557,9 +595,70 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ 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 989e1552d..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
@@ -20,9 +20,9 @@ from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
from vyos.ifconfig import Section
+from vyos.frrender import ospf6_daemon
from vyos.utils.process import process_named_running
-PROCESS_NAME = 'ospf6d'
base_path = ['protocols', 'ospfv3']
route_map = 'foo-bar-baz-0815'
@@ -36,7 +36,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase):
super(TestProtocolsOSPFv3, cls).setUpClass()
# Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
- cls.daemon_pid = process_named_running(PROCESS_NAME)
+ cls.daemon_pid = process_named_running(ospf6_daemon)
cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit'])
cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '20', 'action', 'permit'])
@@ -54,8 +54,13 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase):
self.cli_delete(base_path)
self.cli_commit()
+ 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(PROCESS_NAME))
+ 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'
@@ -78,7 +83,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf6', 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)
@@ -86,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}', daemon=PROCESS_NAME)
+ 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])
@@ -107,7 +112,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf6', 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)
@@ -131,7 +136,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf6', 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)
@@ -162,13 +167,13 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf6', stop_section='^exit')
self.assertIn(f'router ospf6', frrconfig)
cost = '100'
priority = '10'
for interface in interfaces:
- if_config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ if_config = self.getFRRconfig(f'interface {interface}', 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)
@@ -185,7 +190,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
for interface in interfaces:
- if_config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ if_config = self.getFRRconfig(f'interface {interface}', stop_section='^exit')
# There should be no OSPF6 configuration at all after interface removal
self.assertNotIn(f' ipv6 ospf6', if_config)
@@ -201,7 +206,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf6', 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)
@@ -227,7 +232,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf6', 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)
@@ -247,7 +252,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf6', 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)
@@ -256,7 +261,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf6', stop_section='^exit')
self.assertIn(f' default-information originate always metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig)
@@ -282,15 +287,15 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf6', 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}', daemon=PROCESS_NAME)
+ 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}', daemon=PROCESS_NAME)
+ 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)
@@ -300,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}', daemon=PROCESS_NAME)
+ 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)
@@ -326,7 +331,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig('router ospf6', 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)
@@ -336,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 ccfced138..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
@@ -19,22 +19,32 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
+from vyos.frrender import pim_daemon
from vyos.ifconfig import Section
from vyos.utils.process import process_named_running
-PROCESS_NAME = 'pimd'
base_path = ['protocols', 'pim']
class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ # call base-classes classmethod
+ super(TestProtocolsPIM, cls).setUpClass()
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
def tearDown(self):
# pimd process must be running
- self.assertTrue(process_named_running(PROCESS_NAME))
+ self.assertTrue(process_named_running(pim_daemon))
self.cli_delete(base_path)
self.cli_commit()
# pimd process must be stopped by now
- self.assertFalse(process_named_running(PROCESS_NAME))
+ self.assertFalse(process_named_running(pim_daemon))
+ # always forward to base class
+ super().tearDown()
def test_01_pim_basic(self):
rp = '127.0.0.1'
@@ -57,11 +67,11 @@ class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR pimd configuration
- frrconfig = self.getFRRconfig(daemon=PROCESS_NAME)
- self.assertIn(f'ip pim rp {rp} {group}', frrconfig)
+ frrconfig = self.getFRRconfig('router pim', stop_section='^exit')
+ self.assertIn(f' rp {rp} {group}', frrconfig)
for interface in interfaces:
- frrconfig = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig(f'interface {interface}', stop_section='^exit')
self.assertIn(f'interface {interface}', frrconfig)
self.assertIn(f' ip pim', frrconfig)
self.assertIn(f' ip pim bfd', frrconfig)
@@ -108,18 +118,18 @@ class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR pimd configuration
- frrconfig = self.getFRRconfig(daemon=PROCESS_NAME)
- self.assertIn(f'ip pim rp {rp} {group}', frrconfig)
- self.assertIn(f'ip pim rp keep-alive-timer {rp_keep_alive_timer}', frrconfig)
- self.assertIn(f'ip pim ecmp rebalance', frrconfig)
- self.assertIn(f'ip pim join-prune-interval {join_prune_interval}', frrconfig)
- self.assertIn(f'ip pim keep-alive-timer {keep_alive_timer}', frrconfig)
- self.assertIn(f'ip pim packets {packets}', frrconfig)
- self.assertIn(f'ip pim register-accept-list {prefix_list}', frrconfig)
- self.assertIn(f'ip pim register-suppress-time {register_suppress_time}', frrconfig)
- self.assertIn(f'no ip pim send-v6-secondary', frrconfig)
- self.assertIn(f'ip pim spt-switchover infinity-and-beyond prefix-list {prefix_list}', frrconfig)
- self.assertIn(f'ip pim ssm prefix-list {prefix_list}', frrconfig)
+ frrconfig = self.getFRRconfig('router pim', 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)
+ self.assertIn(f' join-prune-interval {join_prune_interval}', frrconfig)
+ self.assertIn(f' packets {packets}', frrconfig)
+ self.assertIn(f' keep-alive-timer {keep_alive_timer}', frrconfig)
+ self.assertIn(f' rp keep-alive-timer {rp_keep_alive_timer}', frrconfig)
+ self.assertIn(f' ssm prefix-list {prefix_list}', frrconfig)
+ self.assertIn(f' register-accept-list {prefix_list}', frrconfig)
+ self.assertIn(f' spt-switchover infinity-and-beyond prefix-list {prefix_list}', frrconfig)
+ self.assertIn(f' ecmp rebalance', frrconfig)
def test_03_pim_igmp_proxy(self):
igmp_proxy = ['protocols', 'igmp-proxy']
@@ -170,11 +180,11 @@ class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
- frrconfig = self.getFRRconfig(daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig()
self.assertIn(f'ip igmp watermark-warn {watermark_warning}', frrconfig)
for interface in interfaces:
- frrconfig = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ frrconfig = self.getFRRconfig(f'interface {interface}', stop_section='^exit')
self.assertIn(f'interface {interface}', frrconfig)
self.assertIn(f' ip igmp', frrconfig)
self.assertIn(f' ip igmp version {version}', frrconfig)
@@ -184,9 +194,9 @@ class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase):
for join, join_config in igmp_join.items():
if 'source' in join_config:
for source in join_config['source']:
- self.assertIn(f' ip igmp join {join} {source}', frrconfig)
+ self.assertIn(f' ip igmp join-group {join} {source}', frrconfig)
else:
- self.assertIn(f' ip igmp join {join}', frrconfig)
+ self.assertIn(f' ip igmp join-group {join}', frrconfig)
if __name__ == '__main__':
- unittest.main(verbosity=2)
+ 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 ba24edca2..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
@@ -18,10 +18,11 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
+
from vyos.ifconfig import Section
+from vyos.frrender import pim6_daemon
from vyos.utils.process import process_named_running
-PROCESS_NAME = 'pim6d'
base_path = ['protocols', 'pim6']
class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase):
@@ -30,7 +31,7 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase):
# call base-classes classmethod
super(TestProtocolsPIMv6, cls).setUpClass()
# Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
- cls.daemon_pid = process_named_running(PROCESS_NAME)
+ cls.daemon_pid = process_named_running(pim6_daemon)
# ensure we can also run this test on a live system - so lets clean
# out the current configuration :)
cls.cli_delete(cls, base_path)
@@ -40,7 +41,9 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# check process health and continuity
- self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME))
+ self.assertEqual(self.daemon_pid, process_named_running(pim6_daemon))
+ # always forward to base class
+ super().tearDown()
def test_pim6_01_mld_simple(self):
# commit changes
@@ -52,7 +55,7 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase):
# Verify FRR pim6d configuration
for interface in interfaces:
- config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ config = self.getFRRconfig(f'interface {interface}', stop_section='^exit')
self.assertIn(f'interface {interface}', config)
self.assertIn(f' ipv6 mld', config)
self.assertNotIn(f' ipv6 mld version 1', config)
@@ -65,7 +68,7 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase):
# Verify FRR pim6d configuration
for interface in interfaces:
- config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ config = self.getFRRconfig(f'interface {interface}', stop_section='^exit')
self.assertIn(f'interface {interface}', config)
self.assertIn(f' ipv6 mld', config)
self.assertIn(f' ipv6 mld version 1', config)
@@ -88,9 +91,9 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase):
# Verify FRR pim6d configuration
for interface in interfaces:
- config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ config = self.getFRRconfig(f'interface {interface}', stop_section='^exit')
self.assertIn(f'interface {interface}', config)
- self.assertIn(f' ipv6 mld join ff18::1234', config)
+ self.assertIn(f' ipv6 mld join-group ff18::1234', config)
# Join a source-specific multicast group
for interface in interfaces:
@@ -100,9 +103,9 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase):
# Verify FRR pim6d configuration
for interface in interfaces:
- config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ config = self.getFRRconfig(f'interface {interface}', stop_section='^exit')
self.assertIn(f'interface {interface}', config)
- self.assertIn(f' ipv6 mld join ff38::5678 2001:db8::5678', config)
+ self.assertIn(f' ipv6 mld join-group ff38::5678 2001:db8::5678', config)
def test_pim6_03_basic(self):
interfaces = Section.interfaces('ethernet')
@@ -128,14 +131,14 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR pim6d configuration
- config = self.getFRRconfig(daemon=PROCESS_NAME)
- self.assertIn(f'ipv6 pim join-prune-interval {join_prune_interval}', config)
- self.assertIn(f'ipv6 pim keep-alive-timer {keep_alive_timer}', config)
- self.assertIn(f'ipv6 pim packets {packets}', config)
- self.assertIn(f'ipv6 pim register-suppress-time {register_suppress_time}', config)
+ config = self.getFRRconfig('router pim6', 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}', daemon=PROCESS_NAME)
+ 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)
@@ -143,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 bfc327fd4..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-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,9 +19,9 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.ifconfig import Section
+from vyos.frrender import rip_daemon
from vyos.utils.process import process_named_running
-PROCESS_NAME = 'ripd'
acl_in = '198'
acl_out = '199'
prefix_list_in = 'foo-prefix'
@@ -35,7 +35,7 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase):
def setUpClass(cls):
super(TestProtocolsRIP, cls).setUpClass()
# Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
- cls.daemon_pid = process_named_running(PROCESS_NAME)
+ cls.daemon_pid = process_named_running(rip_daemon)
# ensure we can also run this test on a live system - so lets clean
# out the current configuration :)
cls.cli_delete(cls, base_path)
@@ -66,8 +66,13 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase):
self.cli_delete(base_path)
self.cli_commit()
+ 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(PROCESS_NAME))
+ 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'
@@ -76,7 +81,7 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase):
interfaces = Section.interfaces('ethernet')
neighbors = ['1.2.3.4', '1.2.3.5', '1.2.3.6']
networks = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']
- redistribute = ['bgp', 'connected', 'isis', 'kernel', 'ospf', 'static']
+ redistribute = ['bgp', 'connected', 'isis', 'kernel', 'nhrp', 'ospf', 'static']
timer_garbage = '888'
timer_timeout = '1000'
timer_update = '90'
@@ -113,7 +118,7 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ripd configuration
- frrconfig = self.getFRRconfig('router rip')
+ 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)
@@ -172,12 +177,12 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR configuration
- frrconfig = self.getFRRconfig('router rip')
+ frrconfig = self.getFRRconfig('router rip', stop_section='^exit')
self.assertIn(f'version {tx_version}', frrconfig)
- frrconfig = self.getFRRconfig(f'interface {interface}')
+ 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 0cfb065c6..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-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,9 +19,9 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.ifconfig import Section
+from vyos.frrender import ripng_daemon
from vyos.utils.process import process_named_running
-PROCESS_NAME = 'ripngd'
acl_in = '198'
acl_out = '199'
prefix_list_in = 'foo-prefix'
@@ -36,7 +36,7 @@ class TestProtocolsRIPng(VyOSUnitTestSHIM.TestCase):
# call base-classes classmethod
super(TestProtocolsRIPng, cls).setUpClass()
# Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
- cls.daemon_pid = process_named_running(PROCESS_NAME)
+ cls.daemon_pid = process_named_running(ripng_daemon)
# ensure we can also run this test on a live system - so lets clean
# out the current configuration :)
cls.cli_delete(cls, base_path)
@@ -66,8 +66,13 @@ class TestProtocolsRIPng(VyOSUnitTestSHIM.TestCase):
self.cli_delete(base_path)
self.cli_commit()
+ 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(PROCESS_NAME))
+ 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'
@@ -110,7 +115,7 @@ class TestProtocolsRIPng(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ripng')
+ 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)
@@ -157,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 29f03a26a..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
@@ -19,12 +19,16 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
+from vyos.frrender import bgp_daemon
from vyos.utils.file import read_file
from vyos.utils.process import process_named_running
base_path = ['protocols', 'rpki']
-PROCESS_NAME = 'bgpd'
-
+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'
@@ -108,17 +112,27 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase):
# call base-classes classmethod
super(TestProtocolsRPKI, cls).setUpClass()
# Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
- cls.daemon_pid = process_named_running(PROCESS_NAME)
+ cls.daemon_pid = process_named_running(bgp_daemon)
# ensure we can also run this test on a live system - so lets clean
# out the current configuration :)
cls.cli_delete(cls, base_path)
+ 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(**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(PROCESS_NAME))
+ self.assertEqual(self.daemon_pid, process_named_running(bgp_daemon))
+ # always forward to base class
+ super().tearDown()
def test_rpki(self):
expire_interval = '3600'
@@ -139,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')
- 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 {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'
@@ -180,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'])
- # Verify FRR configuration
- frrconfig = self.getFRRconfig('rpki')
- for cache_name, cache_config in cache.items():
- port = cache_config['port']
- preference = cache_config['preference']
- username = cache_config['username']
- self.assertIn(f'rpki cache {cache_name} {port} {username} /run/frr/id_rpki_{cache_name} /run/frr/id_rpki_{cache_name}.pub preference {preference}', frrconfig)
+ 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])
- # 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)
+ # commit changes
+ self.cli_commit()
+
+ # 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)
# 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','')])
@@ -209,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 {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} ]:
+
+ 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)
+ # 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'])
@@ -235,13 +265,62 @@ 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):
+ 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'
+ port = '8080'
+ preference = '1'
+ username = 'foo'
+ source_address = '100.10.10.1'
+
+ self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', f'{source_address}/24'])
+
+
+ for test_set in [ {'path': base_path, 'frrargs': base_frr_config_args},
+ {'path': vrf_path + base_path, 'frrargs': vrf_frr_config_args} ]:
+
+ if 'vrf' in test_set['path']:
+ self.cli_set(vrf_path + ['table', '1000'])
+
+ # 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(**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 daa7f088f..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
@@ -20,11 +20,11 @@ from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
from vyos.ifconfig import Section
+from vyos.frrender import zebra_daemon
from vyos.utils.process import process_named_running
from vyos.utils.system import sysctl_read
base_path = ['protocols', 'segment-routing']
-PROCESS_NAME = 'zebra'
class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase):
@classmethod
@@ -32,79 +32,290 @@ class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase):
# call base-classes classmethod
super(TestProtocolsSegmentRouting, cls).setUpClass()
# Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
- cls.daemon_pid = process_named_running(PROCESS_NAME)
+ cls.daemon_pid = process_named_running(zebra_daemon)
# ensure we can also run this test on a live system - so lets clean
# out the current configuration :)
cls.cli_delete(cls, base_path)
+ # 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)
self.cli_commit()
# check process health and continuity
- self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME))
+ self.assertEqual(self.daemon_pid, process_named_running(zebra_daemon))
+ # always forward to base class
+ super().tearDown()
def test_srv6(self):
- interfaces = Section.interfaces('ethernet', vlan=False)
locators = {
- 'foo' : { 'prefix' : '2001:a::/64' },
- 'foo' : { 'prefix' : '2001:b::/64', 'usid' : {} },
+ 'foo1': {'prefix': '2001:a::/64'},
+ 'foo2': {'prefix': '2001:b::/64', 'usid': {}},
+ 'foo3': {'prefix': '2001:c::/64', 'format': 'uncompressed-f4024'},
+ 'foo4': {
+ 'prefix': '2001:d::/48',
+ 'block-len': '32',
+ 'node-len': '16',
+ 'func-bits': '12',
+ 'usid': {},
+ 'format': 'usid-f3216',
+ },
}
for locator, locator_config in locators.items():
- self.cli_set(base_path + ['srv6', 'locator', locator, 'prefix', locator_config['prefix']])
+ self.cli_set(
+ base_path
+ + ['srv6', 'locator', locator, 'prefix', locator_config['prefix']]
+ )
+ if 'block-len' in locator_config:
+ self.cli_set(
+ base_path
+ + [
+ 'srv6',
+ 'locator',
+ locator,
+ 'block-len',
+ locator_config['block-len'],
+ ]
+ )
+ if 'node-len' in locator_config:
+ self.cli_set(
+ base_path
+ + [
+ 'srv6',
+ 'locator',
+ locator,
+ 'node-len',
+ locator_config['node-len'],
+ ]
+ )
+ if 'func-bits' in locator_config:
+ self.cli_set(
+ base_path
+ + [
+ 'srv6',
+ 'locator',
+ locator,
+ 'func-bits',
+ locator_config['func-bits'],
+ ]
+ )
if 'usid' in locator_config:
self.cli_set(base_path + ['srv6', 'locator', locator, 'behavior-usid'])
+ if 'format' in locator_config:
+ self.cli_set(
+ base_path
+ + ['srv6', 'locator', locator, 'format', locator_config['format']]
+ )
# verify() - SRv6 should be enabled on at least one interface!
with self.assertRaises(ConfigSessionError):
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:
- self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_enabled'), '1')
- self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_require_hmac'), '0') # default
+ for interface in self._interfaces:
+ self.assertEqual(
+ sysctl_read(['net', 'ipv6', 'conf', interface, 'seg6_enabled']), '1'
+ )
+ self.assertEqual(
+ sysctl_read(['net', 'ipv6', 'conf', interface, 'seg6_require_hmac']), '0'
+ ) # default
- frrconfig = self.getFRRconfig(f'segment-routing', daemon='zebra')
- self.assertIn(f'segment-routing', frrconfig)
- self.assertIn(f' srv6', frrconfig)
- self.assertIn(f' locators', frrconfig)
+ 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 = (
+ 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 {locator_config["prefix"]} block-len 40 node-len 24 func-bits 16', frrconfig)
+ self.assertIn(
+ f' prefix {prefix}{block_len}{node_len}{func_bits}',
+ frrconfig,
+ )
+
+ if 'format' in locator_config:
+ self.assertIn(f' format {locator_config["format"]}', frrconfig)
+ if 'usid' in locator_config:
+ self.assertIn(' behavior usid', frrconfig)
+
+ def test_srv6_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:
- self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_enabled'), '1')
- self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_require_hmac'), '-1') # ignore
+ for interface in self._interfaces:
+ self.assertEqual(
+ sysctl_read(['net', 'ipv6', 'conf', interface, 'seg6_enabled']), '1'
+ )
+ self.assertEqual(
+ 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:
- self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_enabled'), '1')
- self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_require_hmac'), '1') # drop
+ for interface in self._interfaces:
+ self.assertEqual(
+ sysctl_read(['net', 'ipv6', 'conf', interface, 'seg6_enabled']), '1'
+ )
+ self.assertEqual(
+ 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 f676e2a52..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-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,14 +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 os
import unittest
+from time import sleep
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
from vyos.template import is_ipv6
+from vyos.template import get_dhcp_router
from vyos.utils.network import get_interface_config
from vyos.utils.network import get_vrf_tableid
+from vyos.utils.process import process_named_running
+from vyos.xml_ref import default_value
base_path = ['protocols', 'static']
vrf_path = ['protocols', 'vrf']
@@ -33,7 +38,11 @@ routes = {
'192.0.2.110' : { 'distance' : '110', 'interface' : 'eth0' },
'192.0.2.120' : { 'distance' : '120', 'disable' : '' },
'192.0.2.130' : { 'bfd' : '' },
- '192.0.2.140' : { 'bfd_source' : '192.0.2.10' },
+ '192.0.2.131' : { 'bfd' : '',
+ 'bfd_profile' : 'vyos1' },
+ '192.0.2.140' : { 'bfd' : '',
+ 'bfd_source' : '192.0.2.10',
+ 'bfd_profile' : 'vyos2' },
},
'interface' : {
'eth0' : { 'distance' : '130' },
@@ -65,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' },
@@ -114,31 +135,75 @@ routes = {
},
}
+multicast_routes = {
+ '224.0.0.0/24' : {
+ 'next_hop' : {
+ '224.203.0.1' : { },
+ '224.203.0.2' : { 'distance' : '110'},
+ },
+ },
+ '224.1.0.0/24' : {
+ 'next_hop' : {
+ '224.205.0.1' : { 'disable' : {} },
+ '224.205.0.2' : { 'distance' : '110'},
+ },
+ },
+ '224.2.0.0/24' : {
+ 'next_hop' : {
+ '1.2.3.0' : { },
+ '1.2.3.1' : { 'distance' : '110'},
+ },
+ },
+ '224.10.0.0/24' : {
+ 'interface' : {
+ 'eth1' : { 'disable' : {} },
+ 'eth2' : { 'distance' : '110'},
+ },
+ },
+ '224.11.0.0/24' : {
+ 'interface' : {
+ 'eth0' : { },
+ 'eth1' : { 'distance' : '10'},
+ },
+ },
+ '224.12.0.0/24' : {
+ 'interface' : {
+ 'eth0' : { },
+ 'eth1' : { 'distance' : '200'},
+ },
+ },
+}
+
tables = ['80', '81', '82']
class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):
@classmethod
def setUpClass(cls):
super(TestProtocolsStatic, cls).setUpClass()
+ cls.cli_delete(cls, base_path)
cls.cli_delete(cls, ['vrf'])
- cls.cli_set(cls, ['vrf', 'name', 'black', 'table', '43210'])
@classmethod
def tearDownClass(cls):
+ cls.cli_delete(cls, base_path)
cls.cli_delete(cls, ['vrf'])
super(TestProtocolsStatic, cls).tearDownClass()
def tearDown(self):
self.cli_delete(base_path)
+ self.cli_delete(['vrf'])
self.cli_commit()
- v4route = self.getFRRconfig('ip route', end='')
+ 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):
- bfd_profile = 'vyos-test'
+ self.cli_set(['vrf', 'name', 'black', 'table', '43210'])
for route, route_config in routes.items():
route_type = 'route'
if is_ipv6(route):
@@ -156,9 +221,11 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):
if 'vrf' in next_hop_config:
self.cli_set(base + ['next-hop', next_hop, 'vrf', next_hop_config['vrf']])
if 'bfd' in next_hop_config:
- self.cli_set(base + ['next-hop', next_hop, 'bfd', 'profile', bfd_profile ])
- if 'bfd_source' in next_hop_config:
- self.cli_set(base + ['next-hop', next_hop, 'bfd', 'multi-hop', 'source', next_hop_config['bfd_source'], 'profile', bfd_profile])
+ self.cli_set(base + ['next-hop', next_hop, 'bfd'])
+ if 'bfd_profile' in next_hop_config:
+ self.cli_set(base + ['next-hop', next_hop, 'bfd', 'profile', next_hop_config['bfd_profile']])
+ if 'bfd_source' in next_hop_config:
+ self.cli_set(base + ['next-hop', next_hop, 'bfd', 'multi-hop', 'source-address', next_hop_config['bfd_source']])
if 'segments' in next_hop_config:
self.cli_set(base + ['next-hop', next_hop, 'segments', next_hop_config['segments']])
@@ -199,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():
@@ -217,9 +284,11 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):
if 'vrf' in next_hop_config:
tmp += ' nexthop-vrf ' + next_hop_config['vrf']
if 'bfd' in next_hop_config:
- tmp += ' bfd profile ' + bfd_profile
- if 'bfd_source' in next_hop_config:
- tmp += ' bfd multi-hop source ' + next_hop_config['bfd_source'] + ' profile ' + bfd_profile
+ tmp += ' bfd'
+ if 'bfd_source' in next_hop_config:
+ tmp += ' multi-hop source ' + next_hop_config['bfd_source']
+ if 'bfd_profile' in next_hop_config:
+ tmp += ' profile ' + next_hop_config['bfd_profile']
if 'segments' in next_hop_config:
tmp += ' segments ' + next_hop_config['segments']
@@ -269,6 +338,7 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):
self.assertIn(tmp, frrconfig)
def test_02_static_table(self):
+ self.cli_set(['vrf', 'name', 'black', 'table', '43210'])
for table in tables:
for route, route_config in routes.items():
route_type = 'route'
@@ -310,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
@@ -363,6 +433,7 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):
def test_03_static_vrf(self):
+ self.cli_set(['vrf', 'name', 'black', 'table', '43210'])
# Create VRF instances and apply the static routes from above to FRR.
# Re-read the configured routes and match them if they are programmed
# properly. This also includes VRF leaking
@@ -426,7 +497,7 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):
self.assertEqual(tmp['linkinfo']['info_kind'], 'vrf')
# Verify FRR bgpd configuration
- frrconfig = self.getFRRconfig(f'vrf {vrf}')
+ frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf')
self.assertIn(f'vrf {vrf}', frrconfig)
# Verify routes
@@ -478,5 +549,238 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):
self.assertIn(tmp, frrconfig)
+ def test_04_static_multicast(self):
+ for route, route_config in multicast_routes.items():
+ if 'next_hop' in route_config:
+ base = base_path + ['mroute', route]
+ for next_hop, next_hop_config in route_config['next_hop'].items():
+ self.cli_set(base + ['next-hop', next_hop])
+ if 'distance' in next_hop_config:
+ self.cli_set(base + ['next-hop', next_hop, 'distance', next_hop_config['distance']])
+ if 'disable' in next_hop_config:
+ self.cli_set(base + ['next-hop', next_hop, 'disable'])
+
+ if 'interface' in route_config:
+ base = base_path + ['mroute', route]
+ for next_hop, next_hop_config in route_config['interface'].items():
+ self.cli_set(base + ['interface', next_hop])
+ if 'distance' in next_hop_config:
+ self.cli_set(base + ['interface', next_hop, 'distance', next_hop_config['distance']])
+
+ self.cli_commit()
+
+ # Verify FRR configuration
+ frrconfig = self.getFRRconfig('ip mroute', end_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():
+ tmp = f'ip mroute {route} {next_hop}'
+ if 'distance' in next_hop_config:
+ tmp += ' ' + next_hop_config['distance']
+ if 'disable' in next_hop_config:
+ self.assertNotIn(tmp, frrconfig)
+ else:
+ self.assertIn(tmp, frrconfig)
+
+ if 'next_hop_interface' in route_config:
+ for next_hop, next_hop_config in route_config['next_hop_interface'].items():
+ tmp = f'ip mroute {route} {next_hop}'
+ if 'distance' in next_hop_config:
+ tmp += ' ' + next_hop_config['distance']
+ if 'disable' in next_hop_config:
+ self.assertNotIn(tmp, frrconfig)
+ else:
+ self.assertIn(tmp, frrconfig)
+
+ def test_05_dhcp_default_route(self):
+ # When running via vyos-build under the QEMU environment a local DHCP
+ # server is available. This test verifies that the default route is set.
+ # When not running under the VyOS QEMU environment, this test is skipped.
+ if not os.path.exists('/tmp/vyos.smoketests.hint'):
+ self.skipTest('Not running under VyOS CI/CD QEMU environment!')
+
+ interface = 'eth0'
+ interface_path = ['interfaces', 'ethernet', interface]
+ default_distance = default_value(interface_path + ['dhcp-options', 'default-route-distance'])
+ self.cli_set(interface_path + ['address', 'dhcp'])
+ self.cli_commit()
+
+ # Wait for dhclient to receive IP address and default gateway
+ sleep(5)
+
+ router = get_dhcp_router(interface)
+ frrconfig = self.getFRRconfig()
+ self.assertIn(rf'ip route 0.0.0.0/0 {router} {interface} tag 210 {default_distance}', frrconfig)
+
+ # T6991: Default route is missing when there is no "protocols static"
+ # CLI node entry
+ self.cli_delete(base_path)
+ # We can trigger a FRR reconfiguration and config re-rendering when
+ # we simply disable IPv6 forwarding
+ self.cli_set(['system', 'ipv6', 'disable-forwarding'])
+ self.cli_commit()
+
+ # Re-check FRR configuration that default route is still present
+ frrconfig = self.getFRRconfig()
+ self.assertIn(rf'ip route 0.0.0.0/0 {router} {interface} tag 210 {default_distance}', frrconfig)
+
+ self.cli_delete(interface_path + ['address'])
+ self.cli_commit()
+
+ # Wait for dhclient to stop
+ while process_named_running('dhclient', cmdline=interface, timeout=10):
+ sleep(0.250)
+
+ 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, failfast=True)
+ 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_static_multicast.py b/smoketest/scripts/cli/test_protocols_static_multicast.py
deleted file mode 100755
index 9fdda236f..000000000
--- a/smoketest/scripts/cli/test_protocols_static_multicast.py
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2024 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import unittest
-
-from base_vyostest_shim import VyOSUnitTestSHIM
-
-
-base_path = ['protocols', 'static', 'multicast']
-
-
-class TestProtocolsStaticMulticast(VyOSUnitTestSHIM.TestCase):
-
- def tearDown(self):
- self.cli_delete(base_path)
- self.cli_commit()
-
- mroute = self.getFRRconfig('ip mroute', end='')
- self.assertFalse(mroute)
-
- def test_01_static_multicast(self):
-
- self.cli_set(base_path + ['route', '224.202.0.0/24', 'next-hop', '224.203.0.1'])
- self.cli_set(base_path + ['interface-route', '224.203.0.0/24', 'next-hop-interface', 'eth0'])
-
- self.cli_commit()
-
- # Verify FRR bgpd configuration
- frrconfig = self.getFRRconfig('ip mroute', end='')
-
- self.assertIn('ip mroute 224.202.0.0/24 224.203.0.1', frrconfig)
- self.assertIn('ip mroute 224.203.0.0/24 eth0', frrconfig)
-
-
-if __name__ == '__main__':
- unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_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 b98c0e9b7..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
@@ -21,30 +21,49 @@ from json import loads
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
-from vyos.ifconfig import Section
+from vyos.ifconfig import Section, Interface
+from vyos.qos import CAKE
from vyos.utils.process import cmd
base_path = ['qos']
-def get_tc_qdisc_json(interface) -> dict:
+
+def get_tc_qdisc_json(interface, all=False) -> dict:
tmp = cmd(f'tc -detail -json qdisc show dev {interface}')
tmp = loads(tmp)
+
+ if all:
+ return tmp
+
return next(iter(tmp))
-def get_tc_filter_json(interface, direction) -> list:
- if direction not in ['ingress', 'egress']:
+
+def get_tc_filter_json(interface, direction=None) -> list:
+ if direction not in ['ingress', 'egress', None]:
raise ValueError()
- tmp = cmd(f'tc -detail -json filter show dev {interface} {direction}')
+
+ cmd_stmt = f'tc -detail -json filter show dev {interface}'
+ if direction:
+ cmd_stmt += f' {direction}'
+
+ tmp = cmd(cmd_stmt)
tmp = loads(tmp)
return tmp
-def get_tc_filter_details(interface, direction) -> list:
+
+def get_tc_filter_details(interface, direction=None) -> list:
# json doesn't contain all params, such as mtu
- if direction not in ['ingress', 'egress']:
+ if direction not in ['ingress', 'egress', None]:
raise ValueError()
- tmp = cmd(f'tc -details filter show dev {interface} {direction}')
+
+ cmd_stmt = f'tc -details filter show dev {interface}'
+ if direction:
+ cmd_stmt += f' {direction}'
+
+ tmp = cmd(cmd_stmt)
return tmp
+
class TestQoS(VyOSUnitTestSHIM.TestCase):
@classmethod
def setUpClass(cls):
@@ -67,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
@@ -223,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
@@ -336,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):
@@ -754,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)
@@ -854,6 +875,455 @@ class TestQoS(VyOSUnitTestSHIM.TestCase):
self.cli_set(['qos', 'traffic-match-group', '3', 'match-group', 'unexpected'])
self.cli_commit()
+ def test_17_cake_updates(self):
+ bandwidth = 1000000
+ rtt = 200
+ interface = self._interfaces[0]
+ policy_name = f'qos-policy-{interface}'
+
+ self.cli_set(base_path + ['interface', interface, 'egress', policy_name])
+ self.cli_set(
+ base_path + ['policy', 'cake', policy_name, 'bandwidth', str(bandwidth)]
+ )
+ self.cli_set(base_path + ['policy', 'cake', policy_name, 'rtt', str(rtt)])
+ 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()
+
+ tmp = get_tc_qdisc_json(interface)
+
+ self.assertEqual('cake', tmp['kind'])
+ # TC store rates as a 32-bit unsigned integer in bps (Bytes per second)
+ self.assertEqual(int(bandwidth * 125), tmp['options']['bandwidth'])
+ # RTT internally is in us
+ self.assertEqual(int(rtt * 1000), tmp['options']['rtt'])
+ self.assertEqual('triple-isolate', tmp['options']['flowmode'])
+ self.assertFalse(tmp['options']['ingress'])
+ self.assertFalse(tmp['options']['nat'])
+ self.assertTrue(tmp['options']['raw'])
+ 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 [
+ 'blind',
+ 'src-host',
+ 'dst-host',
+ 'dual-dst-host',
+ 'dual-src-host',
+ 'triple-isolate',
+ 'flow',
+ 'host',
+ ]:
+ self.cli_set(
+ base_path
+ + ['policy', 'cake', policy_name, 'flow-isolation', flow_isolation]
+ )
+
+ if nat:
+ self.cli_set(
+ base_path + ['policy', 'cake', policy_name, 'flow-isolation-nat']
+ )
+ else:
+ self.cli_delete(
+ base_path + ['policy', 'cake', policy_name, 'flow-isolation-nat']
+ )
+
+ self.cli_commit()
+
+ tmp = get_tc_qdisc_json(interface)
+ self.assertEqual(
+ CAKE.flow_isolation_map.get(flow_isolation), tmp['options']['flowmode']
+ )
+
+ self.assertEqual(nat, tmp['options']['nat'])
+ nat = not nat
+
+ def test_18_priority_queue_default(self):
+ interface = self._interfaces[0]
+ policy_name = f'qos-policy-{interface}'
+
+ self.cli_set(base_path + ['interface', interface, 'egress', policy_name])
+ self.cli_set(
+ base_path
+ + ['policy', 'priority-queue', policy_name, 'description', 'default policy']
+ )
+
+ self.cli_commit()
+
+ tmp = get_tc_qdisc_json(interface, all=True)
+
+ self.assertEqual(2, len(tmp))
+ self.assertEqual('prio', tmp[0]['kind'])
+ self.assertDictEqual(
+ {
+ 'bands': 2,
+ 'priomap': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
+ 'multiqueue': False,
+ },
+ tmp[0]['options'],
+ )
+ self.assertEqual('pfifo', tmp[1]['kind'])
+ self.assertDictEqual({'limit': 1000}, tmp[1]['options'])
+
+ def test_19_priority_queue_default_random_detect(self):
+ interface = self._interfaces[0]
+ policy_name = f'qos-policy-{interface}'
+
+ self.cli_set(base_path + ['interface', interface, 'egress', policy_name])
+ self.cli_set(
+ base_path
+ + [
+ 'policy',
+ 'priority-queue',
+ policy_name,
+ 'default',
+ 'queue-type',
+ 'random-detect',
+ ]
+ )
+
+ self.cli_commit()
+
+ tmp = get_tc_qdisc_json(interface, all=True)
+
+ self.assertEqual(2, len(tmp))
+ self.assertEqual('prio', tmp[0]['kind'])
+ self.assertDictEqual(
+ {
+ 'bands': 2,
+ 'priomap': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
+ 'multiqueue': False,
+ },
+ tmp[0]['options'],
+ )
+ self.assertEqual('red', tmp[1]['kind'])
+ self.assertDictEqual(
+ {
+ 'limit': 73728,
+ 'min': 9216,
+ 'max': 18432,
+ 'ecn': False,
+ 'harddrop': False,
+ 'adaptive': False,
+ 'nodrop': False,
+ 'ewma': 3,
+ 'probability': 0.1,
+ 'Scell_log': 13,
+ },
+ tmp[1]['options'],
+ )
+
+ def test_20_round_robin_policy_default(self):
+ interface = self._interfaces[0]
+ policy_name = f'qos-policy-{interface}'
+
+ self.cli_set(base_path + ['interface', interface, 'egress', policy_name])
+ self.cli_set(
+ base_path
+ + ['policy', 'round-robin', policy_name, 'description', 'default policy']
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ tmp = get_tc_qdisc_json(interface, all=True)
+
+ self.assertEqual(2, len(tmp))
+ self.assertEqual('drr', tmp[0]['kind'])
+ self.assertDictEqual({}, tmp[0]['options'])
+ self.assertEqual('sfq', tmp[1]['kind'])
+ self.assertDictEqual(
+ {
+ 'limit': 127,
+ 'quantum': 1514,
+ 'depth': 127,
+ 'flows': 128,
+ 'divisor': 1024,
+ },
+ tmp[1]['options'],
+ )
+
+ tmp = get_tc_filter_json(interface)
+ self.assertEqual(3, len(tmp))
+
+ for rec in tmp:
+ self.assertEqual('u32', rec['kind'])
+ self.assertEqual(1, rec['pref'])
+ self.assertEqual('all', rec['protocol'])
+
+ self.assertDictEqual(
+ {
+ 'fh': '800::800',
+ 'order': 2048,
+ 'key_ht': '800',
+ 'bkt': '0',
+ 'flowid': '1:1',
+ 'not_in_hw': True,
+ 'match': {'value': '0', 'mask': '0', 'offmask': '', 'off': 0},
+ },
+ tmp[2]['options'],
+ )
+
+ def test_21_shaper_hfsc(self):
+ interface = self._interfaces[0]
+ policy_name = f'qos-policy-{interface}'
+ ul = {
+ 'm1': '100kbit',
+ 'm2': '150kbit',
+ 'd': '100',
+ }
+ ls = {'m2': '120kbit'}
+ rt = {
+ 'm1': '110kbit',
+ 'm2': '130kbit',
+ 'd': '75',
+ }
+ self.cli_set(base_path + ['interface', interface, 'egress', policy_name])
+ self.cli_set(base_path + ['policy', 'shaper-hfsc', policy_name])
+
+ # Policy {policy_name} misses "default" class!
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(
+ base_path + ['policy', 'shaper-hfsc', policy_name, 'default', 'upperlimit']
+ )
+
+ # At least one m2 value needs to be set for class: {class_name}
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(
+ base_path + ['policy', 'shaper-hfsc', policy_name, 'default', 'upperlimit', 'm1', ul['m1']]
+ )
+ # {class_name} upperlimit m1 value is set, but no m2 was found!
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(
+ base_path + ['policy', 'shaper-hfsc', policy_name, 'default', 'upperlimit', 'm2', ul['m2']]
+ )
+ # {class_name} upperlimit m1 value is set, but no d was found!
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(
+ base_path + ['policy', 'shaper-hfsc', policy_name, 'default', 'upperlimit', 'd', ul['d']]
+ )
+ # Linkshare m2 needs to be defined to use upperlimit m2 for class: {class_name}
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(
+ base_path + ['policy', 'shaper-hfsc', policy_name, 'default', 'linkshare', 'm2', ls['m2']]
+ )
+ self.cli_commit()
+
+ # use raw because tc json is incorrect here
+ tmp = cmd(f'tc -details qdisc show dev {interface}')
+ for rec in tmp.split('\n'):
+ rec = rec.strip()
+ if 'root' in rec:
+ self.assertEqual(rec, 'qdisc hfsc 1: root refcnt 2 default 2')
+ else:
+ self.assertRegex(
+ rec,
+ r'qdisc sfq \S+: parent 1:2 limit 127p quantum 1514b depth 127 flows 128 divisor 1024 perturb 10sec',
+ )
+ # use raw because tc json is incorrect here
+ tmp = cmd(f'tc -details class show dev {interface}')
+ for rec in tmp.split('\n'):
+ rec = rec.strip().lower()
+ if 'root' in rec:
+ self.assertEqual(rec, 'class hfsc 1: root')
+ elif 'hfsc 1:1' in rec:
+ # m2 \S+bit is auto bandwidth
+ self.assertRegex(
+ rec,
+ r'class hfsc 1:1 parent 1: sc m1 0bit d 0us m2 \S+bit ul m1 0bit d 0us m2 \S+bit',
+ )
+ else:
+ self.assertRegex(
+ rec,
+ rf'class hfsc 1:2 parent 1:1 leaf \S+: ls m1 0bit d 0us m2 {ls["m2"]} ul m1 {ul["m1"]} d {ul["d"]}ms m2 {ul["m2"]}',
+ )
+
+ for key, val in rt.items():
+ self.cli_set(
+ base_path + ['policy', 'shaper-hfsc', policy_name, 'default', 'realtime', key, val]
+ )
+ self.cli_commit()
+
+ tmp = cmd(f'tc -details class show dev {interface}')
+ for rec in tmp.split('\n'):
+ rec = rec.strip().lower()
+ if 'hfsc 1:2' in rec:
+ self.assertTrue(
+ f'rt m1 {rt["m1"]} d {rt["d"]}ms m2 {rt["m2"]} ls m1 0bit d 0us m2 {ls["m2"]} ul m1 {ul["m1"]} d {ul["d"]}ms m2 {ul["m2"]}'
+ in rec
+ )
+
+ # add some class
+ self.cli_set(
+ base_path + ['policy', 'shaper-hfsc', policy_name, 'class', '10', 'linkshare', 'm2', '300kbit']
+ )
+ self.cli_set(
+ base_path + ['policy', 'shaper-hfsc', policy_name, 'class', '10', 'match', 'tst', 'ip', 'dscp', 'internet']
+ )
+
+ self.cli_set(
+ base_path + ['policy', 'shaper-hfsc', policy_name, 'class', '30', 'realtime', 'm2', '250kbit']
+ )
+ self.cli_set(
+ base_path + ['policy', 'shaper-hfsc', policy_name, 'class', '30', 'realtime', 'd', '77']
+ )
+ self.cli_set(
+ base_path + ['policy', 'shaper-hfsc', policy_name, 'class', '30', 'match', 'tst30', 'ip', 'dscp', 'critical']
+ )
+ self.cli_commit()
+
+ tmp = cmd(f'tc -details qdisc show dev {interface}')
+ self.assertEqual(4, len(tmp.split('\n')))
+
+ tmp = cmd(f'tc -details class show dev {interface}')
+ tmp = tmp.lower()
+
+ self.assertTrue(
+ f'rt m1 {rt["m1"]} d {rt["d"]}ms m2 {rt["m2"]} ls m1 0bit d 0us m2 {ls["m2"]} ul m1 {ul["m1"]} d {ul["d"]}ms m2 {ul["m2"]}'
+ in tmp
+ )
+ self.assertTrue(': ls m1 0bit d 0us m2 300kbit' in tmp)
+ self.assertTrue(': rt m1 0bit d 77ms m2 250kbit' in tmp)
+
+ def test_22_rate_control_default(self):
+ interface = self._interfaces[0]
+ policy_name = f'qos-policy-{interface}'
+ bandwidth = 5000
+
+ self.cli_set(base_path + ['interface', interface, 'egress', policy_name])
+ self.cli_set(base_path + ['policy', 'rate-control', policy_name])
+ with self.assertRaises(ConfigSessionError):
+ # Bandwidth not defined
+ self.cli_commit()
+
+ self.cli_set(base_path + ['policy', 'rate-control', policy_name, 'bandwidth', str(bandwidth)])
+ # commit changes
+ self.cli_commit()
+
+ tmp = get_tc_qdisc_json(interface)
+
+ self.assertEqual('tbf', tmp['kind'])
+ # TC store rates as a 32-bit unsigned integer in bps (Bytes per second)
+ self.assertEqual(int(bandwidth * 125), tmp['options']['rate'])
+
+ def test_23_policy_limiter_iif_filter(self):
+ policy_name = 'smoke_test'
+ base_policy_path = ['qos', 'policy', 'limiter', policy_name]
+
+ self.cli_set(['qos', 'interface', self._interfaces[0], 'ingress', policy_name])
+ self.cli_set(base_policy_path + ['class', '100', 'bandwidth', '20gbit'])
+ self.cli_set(base_policy_path + ['class', '100', 'burst', '3760k'])
+ self.cli_set(base_policy_path + ['class', '100', 'match', 'test', 'interface', self._interfaces[0]])
+ self.cli_set(base_policy_path + ['class', '100', 'priority', '20'])
+ self.cli_set(base_policy_path + ['default', 'bandwidth', '1gbit'])
+ self.cli_set(base_policy_path + ['default', 'burst', '125000000b'])
+ self.cli_commit()
+
+ iif = Interface(self._interfaces[0]).get_ifindex()
+ tc_filters = cmd(f'tc filter show dev {self._interfaces[0]} ingress')
+
+ # class 100
+ self.assertIn('filter parent ffff: protocol all pref 20 basic chain 0', tc_filters)
+ self.assertIn(f'meta(rt_iif eq {iif})', tc_filters)
+ self.assertIn('action order 1: police 0x1 rate 20Gbit burst 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)
+
+ def test_24_policy_shaper_match_ether(self):
+ interface = self._interfaces[0]
+ bandwidth = 250
+ default_bandwidth = 20
+ default_ceil = 30
+ class_bandwidth = 50
+ class_ceil = 80
+
+ shaper_name = f'qos-shaper-{interface}'
+
+ self.cli_set(base_path + ['interface', interface, 'egress', shaper_name])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'bandwidth', f'{bandwidth}mbit'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'bandwidth', f'{default_bandwidth}mbit'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'ceiling', f'{default_ceil}mbit'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'queue-type', 'fair-queue'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '23', 'bandwidth', f'{class_bandwidth}mbit'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '23', 'ceiling', f'{class_ceil}mbit'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '23', 'match', '10', 'ether', 'protocol', 'all'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '23', 'match', '10', 'ether', 'destination', '0c:89:0a:2e:00:00'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '23', 'match', '10', 'ether', 'source', '0c:89:0a:2e:00:01'])
+
+ # commit changes
+ self.cli_commit()
+
+ config_entries = (
+ f'root rate {bandwidth}Mbit ceil {bandwidth}Mbit',
+ f'prio 0 rate {class_bandwidth}Mbit ceil {class_ceil}Mbit',
+ f'prio 7 rate {default_bandwidth}Mbit ceil {default_ceil}Mbit'
+ )
+
+ output = cmd(f'tc class show dev {interface}')
+
+ for config_entry in config_entries:
+ self.assertIn(config_entry, output)
+
+ filter = get_tc_filter_details(interface)
+ self.assertIn('match 0c890a2e/ffffffff at -8', filter)
+ self.assertIn('match 00010000/ffff0000 at -4', filter)
+ self.assertIn('match 00000c89/0000ffff at -16', filter)
+ self.assertIn('match 0a2e0000/ffffffff at -12', filter)
+
+ for proto in ['802.1Q', '802_2', '802_3', 'aarp', 'aoe', 'arp', 'atalk',
+ 'dec', 'ip', 'ipv6', 'ipx', 'lat', 'localtalk', 'rarp',
+ 'snap', 'x25', 1, 255, 65535]:
+ self.cli_set(
+ base_path + ['policy', 'shaper', shaper_name, 'class', '23',
+ 'match', '10', 'ether', 'protocol', str(proto)])
+ self.cli_commit()
+
+ if isinstance(proto, int):
+ if proto == 1:
+ self.assertIn(f'filter parent 1: protocol 802_3 pref',
+ get_tc_filter_details(interface))
+ else:
+ self.assertIn(f'filter parent 1: protocol [{proto}] pref',
+ get_tc_filter_details(interface))
+
+ elif proto == '0x000C':
+ # see other codes in the iproute2 eg https://github.com/iproute2/iproute2/blob/413cf4f03a9b6a219c94b86f41d67992b0a14b82/include/uapi/linux/if_ether.h#L130
+ self.assertIn(f'filter parent 1: protocol can pref',
+ get_tc_filter_details(interface))
+
+ else:
+ self.assertIn(f'filter parent 1: protocol {proto} pref',
+ get_tc_filter_details(interface))
+
if __name__ == '__main__':
- unittest.main(verbosity=2)
+ 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 46c4e25a1..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-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
@@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
+import re
import unittest
from json import loads
@@ -22,15 +23,20 @@ from json import loads
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
+from vyos.kea import kea_add_lease
+from vyos.kea import kea_delete_lease
+from vyos.utils.process import cmd
from vyos.utils.process import process_named_running
from vyos.utils.file import read_file
from vyos.template import inc_ip
from vyos.template import dec_ip
PROCESS_NAME = 'kea-dhcp4'
-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'
subnet = '192.0.2.0/25'
@@ -39,15 +45,18 @@ dns_1 = inc_ip(subnet, 2)
dns_2 = inc_ip(subnet, 3)
domain_name = 'vyos.net'
+
class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
@classmethod
def setUpClass(cls):
super(TestServiceDHCPServer, cls).setUpClass()
- # Clear out current configuration to allow running this test on a live system
+ # Clear out current configuration to allow running this test on a live system
cls.cli_delete(cls, base_path)
cidr_mask = subnet.split('/')[-1]
- cls.cli_set(cls, ['interfaces', 'dummy', interface, 'address', f'{router}/{cidr_mask}'])
+ cls.cli_set(
+ cls, ['interfaces', 'dummy', interface, 'address', f'{router}/{cidr_mask}']
+ )
@classmethod
def tearDownClass(cls):
@@ -57,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
@@ -69,7 +80,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
self.assertTrue(isinstance(current, list), msg=f'Failed path: {path}')
self.assertTrue(0 <= key < len(current), msg=f'Failed path: {path}')
else:
- assert False, "Invalid type"
+ assert False, 'Invalid type'
current = current[key]
@@ -88,19 +99,113 @@ 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'
range_0_start = inc_ip(subnet, 10)
- range_0_stop = inc_ip(subnet, 20)
+ range_0_stop = inc_ip(subnet, 20)
range_1_start = inc_ip(subnet, 40)
- range_1_stop = inc_ip(subnet, 50)
+ range_1_stop = inc_ip(subnet, 50)
+
+ self.setup_single_pool_range(
+ range_0_start, range_0_stop, range_1_start, range_1_stop, shared_net_name
+ )
+
+ # 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_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])
@@ -110,66 +215,210 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
# 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
+ 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()
- 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)
+ def test_dhcp_vendor_option_ubiquiti(self):
+ shared_net_name = 'SMOKE-1'
- # Verify options
- self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'domain-name', 'data': domain_name})
- self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, '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})
+ 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)
- # Verify pools
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
- {'pool': f'{range_0_start} - {range_0_stop}'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'option-data'],
+ {'name': 'vendor-encapsulated-options'},
+ )
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.assertTrue(process_named_running(PROCESS_NAME))
+ 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'
- range_0_start = inc_ip(subnet, 10)
- range_0_stop = inc_ip(subnet, 20)
- smtp_server = '1.2.3.4'
- time_server = '4.3.2.1'
- tftp_server = 'tftp.vyos.io'
- search_domains = ['foo.vyos.net', 'bar.vyos.net']
- bootfile_name = 'vyos'
- bootfile_server = '192.0.2.1'
- wpad = 'http://wpad.vyos.io/foo/bar'
- server_identifier = bootfile_server
+ range_0_start = inc_ip(subnet, 10)
+ range_0_stop = inc_ip(subnet, 20)
+ smtp_server = '1.2.3.4'
+ time_server = '4.3.2.1'
+ tftp_server = 'tftp.vyos.io'
+ search_domains = ['foo.vyos.net', 'bar.vyos.net']
+ bootfile_name = 'vyos'
+ bootfile_server = '192.0.2.1'
+ wpad = 'http://wpad.vyos.io/foo/bar'
+ server_identifier = bootfile_server
ipv6_only_preferred = '300'
+ capwap_access_controller = '192.168.2.125'
+ interface_mtu = '1420'
pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
self.cli_set(pool + ['subnet-id', '1'])
@@ -178,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])
@@ -189,8 +439,15 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
self.cli_set(pool + ['option', 'bootfile-server', bootfile_server])
self.cli_set(pool + ['option', 'wpad-url', wpad])
self.cli_set(pool + ['option', 'server-identifier', server_identifier])
+ self.cli_set(pool + ['option', 'capwap-controller', capwap_access_controller])
+
+ static_route = '10.0.0.0/24'
+ static_route_nexthop = '192.0.2.1'
- self.cli_set(pool + ['option', 'static-route', '10.0.0.0/24', 'next-hop', '192.0.2.1'])
+ self.cli_set(
+ pool
+ + ['option', 'static-route', static_route, 'next-hop', static_route_nexthop]
+ )
self.cli_set(pool + ['option', 'ipv6-only-preferred', ipv6_only_preferred])
self.cli_set(pool + ['option', 'time-zone', 'Europe/London'])
@@ -203,95 +460,143 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
config = read_file(KEA4_CONF)
obj = loads(config)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'boot-file-name', bootfile_name)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'next-server', bootfile_server)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400)
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name
+ )
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet
+ )
+ self.verify_config_value(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4'],
+ 'boot-file-name',
+ bootfile_name,
+ )
+ self.verify_config_value(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4'],
+ 'next-server',
+ bootfile_server,
+ )
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400
+ )
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400
+ )
# Verify options
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'domain-name', 'data': domain_name})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'boot-file-name', 'data': bootfile_name},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name', 'data': domain_name},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'domain-search', 'data': ', '.join(search_domains)})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'pop-server', 'data': smtp_server})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'domain-search', 'data': ', '.join(search_domains)},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'smtp-server', 'data': smtp_server})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'pop-server', 'data': smtp_server},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'time-servers', 'data': time_server})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'smtp-server', 'data': smtp_server},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'routers', 'data': router})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'time-servers', 'data': time_server},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'dhcp-server-identifier', 'data': server_identifier})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'tftp-server-name', 'data': tftp_server})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'dhcp-server-identifier', 'data': server_identifier},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'wpad-url', 'data': wpad})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'capwap-ac-v4', 'data': capwap_access_controller},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'rfc3442-static-route', 'data': '24,10,0,0,192,0,2,1, 0,192,0,2,1'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'tftp-server-name', 'data': tftp_server},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'windows-static-route', 'data': '24,10,0,0,192,0,2,1'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'wpad-url', 'data': wpad},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'v6-only-preferred', 'data': ipv6_only_preferred})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {
+ 'name': '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': 'ip-forwarding', 'data': "true"})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'v6-only-preferred', 'data': ipv6_only_preferred},
+ )
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'ip-forwarding', 'data': 'true'},
+ )
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'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'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'pcode', 'data': 'GMT0BST\\,M3.5.0/1\\,M10.5.0'},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'tcode', 'data': 'Europe/London'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'tcode', 'data': 'Europe/London'},
+ )
# Verify pools
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
- {'pool': f'{range_0_start} - {range_0_stop}'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
+ {'pool': f'{range_0_start} - {range_0_stop}'},
+ )
# Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
+ self.verify_service_running()
def test_dhcp_single_pool_options_scoped(self):
shared_net_name = 'SMOKE-2'
range_0_start = inc_ip(subnet, 10)
- range_0_stop = inc_ip(subnet, 20)
+ range_0_stop = inc_ip(subnet, 20)
range_router = inc_ip(subnet, 5)
range_dns_1 = inc_ip(subnet, 6)
@@ -320,40 +625,58 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
config = read_file(KEA4_CONF)
obj = loads(config)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400)
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name
+ )
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet
+ )
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400
+ )
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400
+ )
# Verify shared-network options
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'option-data'],
- {'name': 'domain-name', 'data': domain_name})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'option-data'],
+ {'name': 'domain-name', 'data': domain_name},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'option-data'],
- {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'option-data'],
+ {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'option-data'],
- {'name': 'routers', 'data': router})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'option-data'],
+ {'name': 'routers', 'data': router},
+ )
# Verify range options
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'],
- {'name': 'domain-name-servers', 'data': f'{range_dns_1}, {range_dns_2}'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'],
+ {'name': 'domain-name-servers', 'data': f'{range_dns_1}, {range_dns_2}'},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'],
- {'name': 'routers', 'data': range_router})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'],
+ {'name': 'routers', 'data': range_router},
+ )
# Verify pool
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], 'pool', f'{range_0_start} - {range_0_stop}')
+ self.verify_config_value(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
+ 'pool',
+ f'{range_0_start} - {range_0_stop}',
+ )
# Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
+ self.verify_service_running()
def test_dhcp_single_pool_static_mapping(self):
shared_net_name = 'SMOKE-2'
@@ -375,18 +698,31 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
for client in ['client1', 'client2', 'client3']:
mac = '00:50:00:00:00:{}'.format(client_base)
self.cli_set(pool + ['static-mapping', client, 'mac', mac])
- self.cli_set(pool + ['static-mapping', client, 'ip-address', inc_ip(subnet, client_base)])
+ self.cli_set(
+ pool
+ + ['static-mapping', client, 'ip-address', inc_ip(subnet, client_base)]
+ )
client_base += 1
# cannot have both mac-address and duid set
with self.assertRaises(ConfigSessionError):
- self.cli_set(pool + ['static-mapping', 'client1', 'duid', '00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:11'])
+ self.cli_set(
+ pool
+ + [
+ 'static-mapping',
+ 'client1',
+ 'duid',
+ '00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:11',
+ ]
+ )
self.cli_commit()
self.cli_delete(pool + ['static-mapping', 'client1', 'duid'])
# cannot have mappings with duplicate IP addresses
self.cli_set(pool + ['static-mapping', 'dupe1', 'mac', '00:50:00:00:fe:ff'])
- self.cli_set(pool + ['static-mapping', 'dupe1', 'ip-address', inc_ip(subnet, 10)])
+ self.cli_set(
+ pool + ['static-mapping', 'dupe1', 'ip-address', inc_ip(subnet, 10)]
+ )
with self.assertRaises(ConfigSessionError):
self.cli_commit()
# Should allow disabled duplicate
@@ -396,47 +732,84 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
# cannot have mappings with duplicate MAC addresses
self.cli_set(pool + ['static-mapping', 'dupe2', 'mac', '00:50:00:00:00:10'])
- self.cli_set(pool + ['static-mapping', 'dupe2', 'ip-address', inc_ip(subnet, 120)])
+ self.cli_set(
+ pool + ['static-mapping', 'dupe2', 'ip-address', inc_ip(subnet, 120)]
+ )
with self.assertRaises(ConfigSessionError):
self.cli_commit()
self.cli_delete(pool + ['static-mapping', 'dupe2'])
-
# cannot have mappings with duplicate MAC addresses
- self.cli_set(pool + ['static-mapping', 'dupe3', 'duid', '00:01:02:03:04:05:06:07:aa:aa:aa:aa:aa:01'])
- self.cli_set(pool + ['static-mapping', 'dupe3', 'ip-address', inc_ip(subnet, 121)])
- self.cli_set(pool + ['static-mapping', 'dupe4', 'duid', '00:01:02:03:04:05:06:07:aa:aa:aa:aa:aa:01'])
- self.cli_set(pool + ['static-mapping', 'dupe4', 'ip-address', inc_ip(subnet, 121)])
+ self.cli_set(
+ pool
+ + [
+ 'static-mapping',
+ 'dupe3',
+ 'duid',
+ '00:01:02:03:04:05:06:07:aa:aa:aa:aa:aa:01',
+ ]
+ )
+ self.cli_set(
+ pool + ['static-mapping', 'dupe3', 'ip-address', inc_ip(subnet, 121)]
+ )
+ self.cli_set(
+ pool
+ + [
+ 'static-mapping',
+ 'dupe4',
+ 'duid',
+ '00:01:02:03:04:05:06:07:aa:aa:aa:aa:aa:01',
+ ]
+ )
+ self.cli_set(
+ pool + ['static-mapping', 'dupe4', 'ip-address', inc_ip(subnet, 121)]
+ )
with self.assertRaises(ConfigSessionError):
self.cli_commit()
self.cli_delete(pool + ['static-mapping', 'dupe3'])
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()
config = read_file(KEA4_CONF)
obj = loads(config)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400)
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name
+ )
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet
+ )
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1
+ )
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400
+ )
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400
+ )
# Verify options
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'domain-name', 'data': domain_name})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name', 'data': domain_name},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'routers', 'data': router})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router},
+ )
client_base = 10
for client in ['client1', 'client2', 'client3']:
@@ -444,14 +817,15 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
ip = inc_ip(subnet, client_base)
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'reservations'],
- {'hostname': client, 'hw-address': mac, 'ip-address': ip})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'reservations'],
+ {'hostname': client, 'hw-address': mac, 'ip-address': ip},
+ )
client_base += 1
# Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
+ self.verify_service_running()
def test_dhcp_multiple_pools(self):
lease_time = '14400'
@@ -463,11 +837,16 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
dns_1 = inc_ip(subnet, 2)
range_0_start = inc_ip(subnet, 10)
- range_0_stop = inc_ip(subnet, 20)
+ range_0_stop = inc_ip(subnet, 20)
range_1_start = inc_ip(subnet, 30)
- range_1_stop = inc_ip(subnet, 40)
-
- pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
+ range_1_stop = inc_ip(subnet, 40)
+
+ pool = base_path + [
+ 'shared-network-name',
+ shared_net_name,
+ 'subnet',
+ subnet,
+ ]
self.cli_set(pool + ['subnet-id', str(int(network) + 1)])
# we use the first subnet IP address as default gateway
self.cli_set(pool + ['option', 'default-router', router])
@@ -484,7 +863,15 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
for client in ['client1', 'client2', 'client3', 'client4']:
mac = '02:50:00:00:00:{}'.format(client_base)
self.cli_set(pool + ['static-mapping', client, 'mac', mac])
- self.cli_set(pool + ['static-mapping', client, 'ip-address', inc_ip(subnet, client_base)])
+ self.cli_set(
+ pool
+ + [
+ 'static-mapping',
+ client,
+ 'ip-address',
+ inc_ip(subnet, client_base),
+ ]
+ )
client_base += 1
# commit changes
@@ -500,37 +887,64 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
dns_1 = inc_ip(subnet, 2)
range_0_start = inc_ip(subnet, 10)
- range_0_stop = inc_ip(subnet, 20)
+ range_0_stop = inc_ip(subnet, 20)
range_1_start = inc_ip(subnet, 30)
- range_1_stop = inc_ip(subnet, 40)
+ range_1_stop = inc_ip(subnet, 40)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'subnet', subnet)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'id', int(network) + 1)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'valid-lifetime', int(lease_time))
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'max-valid-lifetime', int(lease_time))
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name
+ )
+ self.verify_config_value(
+ obj,
+ ['Dhcp4', 'shared-networks', int(network), 'subnet4'],
+ 'subnet',
+ subnet,
+ )
+ self.verify_config_value(
+ obj,
+ ['Dhcp4', 'shared-networks', int(network), 'subnet4'],
+ 'id',
+ int(network) + 1,
+ )
+ self.verify_config_value(
+ obj,
+ ['Dhcp4', 'shared-networks', int(network), 'subnet4'],
+ 'valid-lifetime',
+ int(lease_time),
+ )
+ self.verify_config_value(
+ obj,
+ ['Dhcp4', 'shared-networks', int(network), 'subnet4'],
+ 'max-valid-lifetime',
+ int(lease_time),
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'],
- {'name': 'domain-name', 'data': domain_name})
+ obj,
+ ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name', 'data': domain_name},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'],
- {'name': 'domain-name-servers', 'data': dns_1})
+ obj,
+ ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name-servers', 'data': dns_1},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'],
- {'name': 'routers', 'data': router})
+ obj,
+ ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'],
- {'pool': f'{range_0_start} - {range_0_stop}'})
+ obj,
+ ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'],
+ {'pool': f'{range_0_start} - {range_0_stop}'},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'],
- {'pool': f'{range_1_start} - {range_1_stop}'})
+ obj,
+ ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'],
+ {'pool': f'{range_1_start} - {range_1_stop}'},
+ )
client_base = 60
for client in ['client1', 'client2', 'client3', 'client4']:
@@ -538,25 +952,34 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
ip = inc_ip(subnet, client_base)
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'reservations'],
- {'hostname': client, 'hw-address': mac, 'ip-address': ip})
+ obj,
+ [
+ 'Dhcp4',
+ 'shared-networks',
+ int(network),
+ 'subnet4',
+ 0,
+ 'reservations',
+ ],
+ {'hostname': client, 'hw-address': mac, 'ip-address': ip},
+ )
client_base += 1
# Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
+ self.verify_service_running()
def test_dhcp_exclude_not_in_range(self):
# T3180: verify else path when slicing DHCP ranges and exclude address
# is not part of the DHCP range
range_0_start = inc_ip(subnet, 10)
- range_0_stop = inc_ip(subnet, 20)
+ range_0_stop = inc_ip(subnet, 20)
pool = base_path + ['shared-network-name', 'EXCLUDE-TEST', 'subnet', subnet]
self.cli_set(pool + ['subnet-id', '1'])
self.cli_set(pool + ['option', 'default-router', router])
self.cli_set(pool + ['exclude', router])
+ self.cli_set(pool + ['range', '0', 'option', 'default-router', router])
self.cli_set(pool + ['range', '0', 'start', range_0_start])
self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
@@ -566,33 +989,42 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
config = read_file(KEA4_CONF)
obj = loads(config)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST')
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST'
+ )
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet
+ )
+
+ pool_obj = {
+ 'pool': f'{range_0_start} - {range_0_stop}',
+ 'option-data': [{'name': 'routers', 'data': router}],
+ }
# Verify options
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'routers', 'data': router})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router},
+ )
# Verify pools
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
- {'pool': f'{range_0_start} - {range_0_stop}'})
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], pool_obj
+ )
# Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
+ self.verify_service_running()
def test_dhcp_exclude_in_range(self):
# T3180: verify else path when slicing DHCP ranges and exclude address
# is not part of the DHCP range
range_0_start = inc_ip(subnet, 10)
- range_0_stop = inc_ip(subnet, 100)
+ range_0_stop = inc_ip(subnet, 100)
- # the DHCP exclude addresse is blanked out of the range which is done
+ # 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)
+ exclude_addr = inc_ip(range_0_start, 20)
range_0_stop_excl = dec_ip(exclude_addr, 1)
range_0_start_excl = inc_ip(exclude_addr, 1)
@@ -600,6 +1032,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
self.cli_set(pool + ['subnet-id', '1'])
self.cli_set(pool + ['option', 'default-router', router])
self.cli_set(pool + ['exclude', exclude_addr])
+ self.cli_set(pool + ['range', '0', 'option', 'default-router', router])
self.cli_set(pool + ['range', '0', 'start', range_0_start])
self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
@@ -609,27 +1042,42 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
config = read_file(KEA4_CONF)
obj = loads(config)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST-2')
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST-2'
+ )
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet
+ )
+
+ pool_obj = {
+ 'pool': f'{range_0_start} - {range_0_stop_excl}',
+ 'option-data': [{'name': 'routers', 'data': router}],
+ }
+
+ pool_exclude_obj = {
+ 'pool': f'{range_0_start_excl} - {range_0_stop}',
+ 'option-data': [{'name': 'routers', 'data': router}],
+ }
# Verify options
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'routers', 'data': router})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
- {'pool': f'{range_0_start} - {range_0_stop_excl}'})
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], pool_obj
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
- {'pool': f'{range_0_start_excl} - {range_0_stop}'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
+ pool_exclude_obj,
+ )
# Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
+ self.verify_service_running()
def test_dhcp_relay_server(self):
# Listen on specific address and return DHCP leases from a non
@@ -640,7 +1088,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
relay_router = inc_ip(relay_subnet, 1)
range_0_start = '10.0.1.0'
- range_0_stop = '10.0.250.255'
+ range_0_stop = '10.0.250.255'
pool = base_path + ['shared-network-name', 'RELAY', 'subnet', relay_subnet]
self.cli_set(pool + ['subnet-id', '1'])
@@ -654,31 +1102,37 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
config = read_file(KEA4_CONF)
obj = loads(config)
- self.verify_config_value(obj, ['Dhcp4', 'interfaces-config'], 'interfaces', [f'{interface}/{router}'])
+ self.verify_config_value(
+ obj, ['Dhcp4', 'interfaces-config'], 'interfaces', [f'{interface}/{router}']
+ )
self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'RELAY')
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', relay_subnet)
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', relay_subnet
+ )
# Verify options
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'routers', 'data': relay_router})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': relay_router},
+ )
# Verify pools
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
- {'pool': f'{range_0_start} - {range_0_stop}'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
+ {'pool': f'{range_0_start} - {range_0_stop}'},
+ )
# Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
+ self.verify_service_running()
def test_dhcp_high_availability(self):
shared_net_name = 'FAILOVER'
failover_name = 'VyOS-Failover'
range_0_start = inc_ip(subnet, 10)
- range_0_stop = inc_ip(subnet, 20)
+ range_0_stop = inc_ip(subnet, 20)
pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
self.cli_set(pool + ['subnet-id', '1'])
@@ -695,7 +1149,9 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
failover_local = router
failover_remote = inc_ip(router, 1)
- self.cli_set(base_path + ['high-availability', 'source-address', failover_local])
+ self.cli_set(
+ base_path + ['high-availability', 'source-address', failover_local]
+ )
self.cli_set(base_path + ['high-availability', 'name', failover_name])
self.cli_set(base_path + ['high-availability', 'remote', failover_remote])
self.cli_set(base_path + ['high-availability', 'status', 'primary'])
@@ -708,43 +1164,78 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
obj = loads(config)
# Verify failover
- self.verify_config_value(obj, ['Dhcp4', 'control-socket'], 'socket-name', KEA4_CTRL)
+ self.verify_config_value(
+ obj, ['Dhcp4', 'control-socket'], 'socket-name', KEA4_CTRL
+ )
self.verify_config_object(
obj,
- ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'],
- {'name': os.uname()[1], 'url': f'http://{failover_local}:647/', 'role': 'primary', 'auto-failover': True})
+ [
+ 'Dhcp4',
+ 'hooks-libraries',
+ 0,
+ 'parameters',
+ 'high-availability',
+ 0,
+ 'peers',
+ ],
+ {
+ 'name': os.uname()[1],
+ 'url': f'http://{failover_local}:647/',
+ 'role': 'primary',
+ 'auto-failover': True,
+ },
+ )
self.verify_config_object(
obj,
- ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'],
- {'name': failover_name, 'url': f'http://{failover_remote}:647/', 'role': 'secondary', 'auto-failover': True})
-
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
+ [
+ 'Dhcp4',
+ 'hooks-libraries',
+ 0,
+ 'parameters',
+ 'high-availability',
+ 0,
+ 'peers',
+ ],
+ {
+ 'name': failover_name,
+ 'url': f'http://{failover_remote}:647/',
+ 'role': 'secondary',
+ 'auto-failover': True,
+ },
+ )
+
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name
+ )
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet
+ )
# Verify options
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'routers', 'data': router})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router},
+ )
# Verify pools
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
- {'pool': f'{range_0_start} - {range_0_stop}'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
+ {'pool': f'{range_0_start} - {range_0_stop}'},
+ )
# Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
- self.assertTrue(process_named_running(CTRL_PROCESS_NAME))
+ self.verify_service_running()
def test_dhcp_high_availability_standby(self):
shared_net_name = 'FAILOVER'
failover_name = 'VyOS-Failover'
range_0_start = inc_ip(subnet, 10)
- range_0_stop = inc_ip(subnet, 20)
+ range_0_stop = inc_ip(subnet, 20)
pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
self.cli_set(pool + ['subnet-id', '1'])
@@ -757,7 +1248,9 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
failover_local = router
failover_remote = inc_ip(router, 1)
- self.cli_set(base_path + ['high-availability', 'source-address', failover_local])
+ self.cli_set(
+ base_path + ['high-availability', 'source-address', failover_local]
+ )
self.cli_set(base_path + ['high-availability', 'name', failover_name])
self.cli_set(base_path + ['high-availability', 'remote', failover_remote])
self.cli_set(base_path + ['high-availability', 'status', 'secondary'])
@@ -770,61 +1263,576 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
obj = loads(config)
# Verify failover
- self.verify_config_value(obj, ['Dhcp4', 'control-socket'], 'socket-name', KEA4_CTRL)
+ self.verify_config_value(
+ obj, ['Dhcp4', 'control-socket'], 'socket-name', KEA4_CTRL
+ )
self.verify_config_object(
obj,
- ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'],
- {'name': os.uname()[1], 'url': f'http://{failover_local}:647/', 'role': 'standby', 'auto-failover': True})
+ [
+ 'Dhcp4',
+ 'hooks-libraries',
+ 0,
+ 'parameters',
+ 'high-availability',
+ 0,
+ 'peers',
+ ],
+ {
+ 'name': os.uname()[1],
+ 'url': f'http://{failover_local}:647/',
+ 'role': 'standby',
+ 'auto-failover': True,
+ },
+ )
self.verify_config_object(
obj,
- ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'],
- {'name': failover_name, 'url': f'http://{failover_remote}:647/', 'role': 'primary', 'auto-failover': True})
-
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
+ [
+ 'Dhcp4',
+ 'hooks-libraries',
+ 0,
+ 'parameters',
+ 'high-availability',
+ 0,
+ 'peers',
+ ],
+ {
+ 'name': failover_name,
+ 'url': f'http://{failover_remote}:647/',
+ 'role': 'primary',
+ 'auto-failover': True,
+ },
+ )
+
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name
+ )
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet
+ )
# Verify options
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'routers', 'data': router})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router},
+ )
# Verify pools
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
- {'pool': f'{range_0_start} - {range_0_stop}'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
+ {'pool': f'{range_0_start} - {range_0_stop}'},
+ )
+
+ # Check for running process
+ self.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'])
self.cli_set(['interfaces', 'ethernet', 'eth1', 'vrf', 'SMOKE-DHCP'])
- self.cli_set(['protocols', 'static', 'route', '10.1.10.0/24', 'interface', 'eth1', 'vrf', 'SMOKE-DHCP'])
- self.cli_set(['vrf', 'name', 'SMOKE-DHCP', 'protocols', 'static', 'route', '10.1.10.0/24', 'next-hop', '10.1.1.2'])
+ self.cli_set(
+ [
+ 'protocols',
+ 'static',
+ 'route',
+ '10.1.10.0/24',
+ 'interface',
+ 'eth1',
+ 'vrf',
+ 'SMOKE-DHCP',
+ ]
+ )
+ self.cli_set(
+ [
+ 'vrf',
+ 'name',
+ 'SMOKE-DHCP',
+ 'protocols',
+ 'static',
+ 'route',
+ '10.1.10.0/24',
+ 'next-hop',
+ '10.1.1.2',
+ ]
+ )
self.cli_set(['vrf', 'name', 'SMOKE-DHCP', 'table', '1000'])
- self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'subnet-id', '1'])
- self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'option', 'default-router', '10.1.10.1'])
- self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'option', 'name-server', '1.1.1.1'])
- self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'range', '1', 'start', '10.1.10.10'])
- self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'range', '1', 'stop', '10.1.10.20'])
+ self.cli_set(
+ base_path
+ + [
+ 'shared-network-name',
+ 'SMOKE-DHCP-NETWORK',
+ 'subnet',
+ '10.1.10.0/24',
+ 'subnet-id',
+ '1',
+ ]
+ )
+ self.cli_set(
+ base_path
+ + [
+ 'shared-network-name',
+ 'SMOKE-DHCP-NETWORK',
+ 'subnet',
+ '10.1.10.0/24',
+ 'option',
+ 'default-router',
+ '10.1.10.1',
+ ]
+ )
+ self.cli_set(
+ base_path
+ + [
+ 'shared-network-name',
+ 'SMOKE-DHCP-NETWORK',
+ 'subnet',
+ '10.1.10.0/24',
+ 'option',
+ 'name-server',
+ '1.1.1.1',
+ ]
+ )
+ self.cli_set(
+ base_path
+ + [
+ 'shared-network-name',
+ 'SMOKE-DHCP-NETWORK',
+ 'subnet',
+ '10.1.10.0/24',
+ 'range',
+ '1',
+ 'start',
+ '10.1.10.10',
+ ]
+ )
+ self.cli_set(
+ base_path
+ + [
+ 'shared-network-name',
+ 'SMOKE-DHCP-NETWORK',
+ 'subnet',
+ '10.1.10.0/24',
+ 'range',
+ '1',
+ 'stop',
+ '10.1.10.20',
+ ]
+ )
self.cli_set(base_path + ['listen-address', '10.1.1.1'])
self.cli_commit()
config = read_file(KEA4_CONF)
obj = loads(config)
- self.verify_config_value(obj, ['Dhcp4', 'interfaces-config'], 'interfaces', ['eth1/10.1.1.1'])
+ self.verify_config_value(
+ obj, ['Dhcp4', 'interfaces-config'], 'interfaces', ['eth1/10.1.1.1']
+ )
self.cli_delete(['interfaces', 'ethernet', 'eth1', 'vrf', 'SMOKE-DHCP'])
- self.cli_delete(['protocols', 'static', 'route', '10.1.10.0/24', 'interface', 'eth1', 'vrf'])
+ self.cli_delete(
+ ['protocols', 'static', 'route', '10.1.10.0/24', 'interface', 'eth1', 'vrf']
+ )
self.cli_delete(['vrf', 'name', 'SMOKE-DHCP'])
self.cli_commit()
+ def test_dhcp_hostsd_lease_sync(self):
+ shared_net_name = 'SMOKE-LEASE-SYNC'
+ domain_name = 'sync.private'
+
+ client_range = range(1, 4)
+ subnet_range_start = inc_ip(subnet, 10)
+ subnet_range_stop = inc_ip(subnet, 20)
+
+ def internal_cleanup():
+ for seq in client_range:
+ ip_addr = inc_ip(subnet, seq)
+ kea_delete_lease(4, None, ip_addr)
+ cmd(
+ f'{HOSTSD_CLIENT} --delete-hosts --tag dhcp-server-{ip_addr} --apply'
+ )
+
+ self.addClassCleanup(internal_cleanup)
+
+ pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
+ self.cli_set(pool + ['subnet-id', '1'])
+ self.cli_set(pool + ['option', 'domain-name', domain_name])
+ self.cli_set(pool + ['range', '0', 'start', subnet_range_start])
+ self.cli_set(pool + ['range', '0', 'stop', subnet_range_stop])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(KEA4_CONF)
+ obj = loads(config)
+
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name
+ )
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet
+ )
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1
+ )
+
+ # Verify options
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name', 'data': domain_name},
+ )
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
+ {'pool': f'{subnet_range_start} - {subnet_range_stop}'},
+ )
+
+ # Check for running process
+ self.verify_service_running()
+
+ # All up and running, now test vyos-hostsd store
+
+ # 1. Inject leases into kea
+ for seq in client_range:
+ client = f'client{seq}'
+ mac = f'00:50:00:00:00:{seq:02}'
+ ip = inc_ip(subnet, seq)
+ kea_add_lease(4, '', ip, host_name=client, mac_address=mac)
+
+ # 2. Verify that leases are not available in vyos-hostsd
+ tag_regex = re.escape(f'dhcp-server-{subnet.rsplit(".", 1)[0]}')
+ host_json = cmd(f'{HOSTSD_CLIENT} --get-hosts {tag_regex}')
+ self.assertFalse(host_json.strip('{}'))
+
+ # 3. Restart the service to trigger vyos-hostsd sync and wait for it to start
+ self.assertTrue(process_named_running(PROCESS_NAME, timeout=30))
+
+ # 4. Verify that leases are synced and available in vyos-hostsd
+ tag_regex = re.escape(f'dhcp-server-{subnet.rsplit(".", 1)[0]}')
+ host_json = cmd(f'{HOSTSD_CLIENT} --get-hosts {tag_regex}')
+ self.assertTrue(host_json)
+
+ 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 c39d4467a..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-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,23 +14,24 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import os
import unittest
import tempfile
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
+from vyos.utils.file import read_file
from vyos.utils.process import cmd
-from vyos.utils.process import process_running
+from vyos.utils.process import process_named_running
+from vyos.xml_ref import default_value
DDCLIENT_SYSTEMD_UNIT = '/run/systemd/system/ddclient.service.d/override.conf'
DDCLIENT_CONF = '/run/ddclient/ddclient.conf'
-DDCLIENT_PID = '/run/ddclient/ddclient.pid'
DDCLIENT_PNAME = 'ddclient'
base_path = ['service', 'dns', 'dynamic']
name_path = base_path + ['name']
+default_interval = default_value(base_path + ['interval'])
server = 'ddns.vyos.io'
hostname = 'test.ddns.vyos.io'
zone = 'vyos.io'
@@ -40,20 +41,26 @@ ttl = '300'
interface = 'eth0'
class TestServiceDDNS(VyOSUnitTestSHIM.TestCase):
- def setUp(self):
- # Always start with a clean CLI instance
- self.cli_delete(base_path)
+ @classmethod
+ def setUpClass(cls):
+ super(TestServiceDDNS, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
def tearDown(self):
# Check for running process
- self.assertTrue(process_running(DDCLIENT_PID))
+ self.assertTrue(process_named_running(DDCLIENT_PNAME, timeout=10))
# Delete DDNS configuration
self.cli_delete(base_path)
self.cli_commit()
- # PID file must no londer exist after process exited
- self.assertFalse(os.path.exists(DDCLIENT_PID))
+ # Check for process not running anymore
+ self.assertFalse(process_named_running(DDCLIENT_PNAME))
+ # always forward to base class
+ super().tearDown()
# IPv4 standard DDNS service configuration
def test_01_dyndns_service_standard(self):
@@ -68,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
@@ -78,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
@@ -93,12 +100,14 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase):
# Check the generating config parameters
ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
- # default value 300 seconds
- self.assertIn(f'daemon=300', ddclient_conf)
self.assertIn(f'usev4=ifv4', ddclient_conf)
self.assertIn(f'ifv4={interface}', ddclient_conf)
self.assertIn(f'password=\'{password}\'', ddclient_conf)
+ # Check default interval of 300 seconds
+ systemd_override = read_file(DDCLIENT_SYSTEMD_UNIT)
+ self.assertIn(f'--daemon {default_interval}', systemd_override)
+
for opt in details.keys():
if opt == 'username':
login = details[opt]
@@ -138,7 +147,6 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase):
# Check the generating config parameters
ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
- self.assertIn(f'daemon={interval}', ddclient_conf)
self.assertIn(f'usev6=ifv6', ddclient_conf)
self.assertIn(f'ifv6={interface}', ddclient_conf)
self.assertIn(f'protocol={proto}', ddclient_conf)
@@ -148,6 +156,10 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'min-interval={wait_time}', ddclient_conf)
self.assertIn(f'max-interval={expiry_time_good}', ddclient_conf)
+ # default value 300 seconds
+ systemd_override = read_file(DDCLIENT_SYSTEMD_UNIT)
+ self.assertIn(f'--daemon {interval}', systemd_override)
+
# IPv4+IPv6 dual DDNS service configuration
def test_03_dyndns_service_dual_stack(self):
services = {'cloudflare': {'protocol': 'cloudflare', 'zone': zone},
@@ -162,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']:
@@ -337,9 +349,10 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Check for process in VRF
- systemd_override = cmd(f'cat {DDCLIENT_SYSTEMD_UNIT}')
- self.assertIn(f'ExecStart=ip vrf exec {vrf_name} /usr/bin/ddclient -file {DDCLIENT_CONF}',
- systemd_override)
+ systemd_override = read_file(DDCLIENT_SYSTEMD_UNIT)
+ self.assertIn(f'ExecStart=ip vrf exec {vrf_name} /usr/bin/ddclient ' \
+ f'--file {DDCLIENT_CONF} --cache {DDCLIENT_CONF.replace("conf", "cache")} ' \
+ f'--foreground --daemon {default_interval}', systemd_override)
# Check for process in VRF
proc = cmd(f'ip vrf pids {vrf_name}')
@@ -349,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 8a6386e4f..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
@@ -89,6 +95,7 @@ server {
PROCESS_NAME = 'nginx'
+
class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
@classmethod
def setUpClass(cls):
@@ -111,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'
@@ -120,19 +215,29 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
# verify() - certificates do not exist (yet)
with self.assertRaises(ConfigSessionError):
self.cli_commit()
- self.cli_set(pki_base + ['certificate', cert_name, 'certificate', cert_data.replace('\n','')])
- self.cli_set(pki_base + ['certificate', cert_name, 'private', 'key', key_data.replace('\n','')])
+ self.cli_set(
+ pki_base
+ + ['certificate', cert_name, 'certificate', cert_data.replace('\n', '')]
+ )
+ self.cli_set(
+ pki_base
+ + ['certificate', cert_name, 'private', 'key', key_data.replace('\n', '')]
+ )
self.cli_set(base_path + ['certificates', 'dh-params', dh_name])
# verify() - dh-params do not exist (yet)
with self.assertRaises(ConfigSessionError):
self.cli_commit()
- self.cli_set(pki_base + ['dh', dh_name, 'parameters', dh_1024.replace('\n','')])
+ self.cli_set(
+ pki_base + ['dh', dh_name, 'parameters', dh_1024.replace('\n', '')]
+ )
# verify() - dh-param minimum length is 2048 bit
with self.assertRaises(ConfigSessionError):
self.cli_commit()
- self.cli_set(pki_base + ['dh', dh_name, 'parameters', dh_2048.replace('\n','')])
+ self.cli_set(
+ pki_base + ['dh', dh_name, 'parameters', dh_2048.replace('\n', '')]
+ )
self.cli_commit()
self.assertTrue(process_named_running(PROCESS_NAME))
@@ -154,13 +259,15 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
key = 'MySuperSecretVyOS'
self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+ self.cli_set(base_path + ['api', 'rest'])
+
self.cli_set(base_path + ['listen-address', address])
self.cli_commit()
nginx_config = read_file('/etc/nginx/sites-enabled/default')
self.assertIn(f'listen {address}:{port} ssl;', nginx_config)
- self.assertIn(f'ssl_protocols TLSv1.2 TLSv1.3;', nginx_config) # default
+ self.assertIn('ssl_protocols TLSv1.2 TLSv1.3;', nginx_config) # default
url = f'https://{address}/retrieve'
payload = {'data': '{"op": "showConfig", "path": []}', 'key': f'{key}'}
@@ -180,11 +287,16 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
self.assertEqual(r.status_code, 401)
# Check path config
- payload = {'data': '{"op": "showConfig", "path": ["system", "login"]}', 'key': f'{key}'}
+ payload = {
+ 'data': '{"op": "showConfig", "path": ["system", "login"]}',
+ 'key': f'{key}',
+ }
r = request('POST', url, verify=False, headers=headers, data=payload)
response = r.json()
vyos_user_exists = 'vyos' in response.get('data', {}).get('user', {})
- self.assertTrue(vyos_user_exists, "The 'vyos' user does not exist in the response.")
+ self.assertTrue(
+ vyos_user_exists, "The 'vyos' user does not exist in the response."
+ )
# GraphQL auth test: a missing key will return status code 400, as
# 'key' is a non-nullable field in the schema; an incorrect key is
@@ -208,7 +320,13 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
}}
"""
- r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query_valid_key})
+ r = request(
+ 'POST',
+ graphql_url,
+ verify=False,
+ headers=headers,
+ json={'query': query_valid_key},
+ )
success = r.json()['data']['SystemStatus']['success']
self.assertTrue(success)
@@ -224,7 +342,13 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
}
"""
- r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query_invalid_key})
+ r = request(
+ 'POST',
+ graphql_url,
+ verify=False,
+ headers=headers,
+ json={'query': query_invalid_key},
+ )
success = r.json()['data']['SystemStatus']['success']
self.assertFalse(success)
@@ -240,7 +364,13 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
}
"""
- r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query_no_key})
+ r = request(
+ 'POST',
+ graphql_url,
+ verify=False,
+ headers=headers,
+ json={'query': query_no_key},
+ )
success = r.json()['data']['SystemStatus']['success']
self.assertFalse(success)
@@ -261,7 +391,9 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
}
}
"""
- r = request('POST', graphql_url, verify=False, headers=headers, json={'query': mutation})
+ r = request(
+ 'POST', graphql_url, verify=False, headers=headers, json={'query': mutation}
+ )
token = r.json()['data']['AuthToken']['data']['result']['token']
@@ -284,14 +416,14 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
}
"""
- r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query})
+ r = request(
+ 'POST', graphql_url, verify=False, headers=headers, json={'query': query}
+ )
success = r.json()['data']['ShowVersion']['success']
self.assertTrue(success)
@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 = {}
@@ -304,6 +436,7 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
self.assertEqual(r.status_code, 503)
self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+ self.cli_set(base_path + ['api', 'rest'])
self.cli_commit()
sleep(2)
@@ -320,12 +453,11 @@ 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 = {}
self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+ self.cli_set(base_path + ['api', 'rest'])
self.cli_commit()
payload = {
@@ -337,12 +469,11 @@ 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 = {}
self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+ self.cli_set(base_path + ['api', 'rest'])
self.cli_commit()
payload = {
@@ -354,37 +485,35 @@ 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'
conf_address = '192.0.2.44/32'
self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+ self.cli_set(base_path + ['api', 'rest'])
self.cli_commit()
payload_path = [
- "interfaces",
- "dummy",
- f"{conf_interface}",
- "address",
- f"{conf_address}",
+ 'interfaces',
+ 'dummy',
+ f'{conf_interface}',
+ 'address',
+ f'{conf_address}',
]
- payload = {'data': json.dumps({"op": "set", "path": payload_path}), 'key': key}
+ payload = {'data': json.dumps({'op': 'set', 'path': payload_path}), 'key': key}
r = request('POST', url, verify=False, headers=headers, data=payload)
self.assertEqual(r.status_code, 200)
@ignore_warning(InsecureRequestWarning)
def test_api_config_file(self):
- address = '127.0.0.1'
- key = 'VyOS-key'
url = f'https://{address}/config-file'
headers = {}
self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+ self.cli_set(base_path + ['api', 'rest'])
self.cli_commit()
payload = {
@@ -396,12 +525,11 @@ 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 = {}
self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+ self.cli_set(base_path + ['api', 'rest'])
self.cli_commit()
payload = {
@@ -413,12 +541,11 @@ 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 = {}
self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+ self.cli_set(base_path + ['api', 'rest'])
self.cli_commit()
payload = {
@@ -455,13 +582,12 @@ 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 = {}
self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+ self.cli_set(base_path + ['api', 'rest'])
self.cli_commit()
# load config via HTTP requires nginx config
@@ -498,5 +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 be03179bf..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
@@ -260,6 +262,82 @@ delegate={delegate_2_prefix},{delegate_mask},name={pool_name}"""
tmp = ','.join(vlans)
self.assertIn(f'{interface},{tmp}', conf['ipoe']['vlan-mon'])
+ def test_ipoe_server_static_client_ip_address(self):
+ mac_address = '08:00:27:2f:d8:06'
+ ip_address = '192.0.2.100'
+
+ # Test configuration of local authentication for PPPoE server
+ self.basic_config()
+ # Rewrite authentication from basic_config
+ self.set(
+ [
+ 'authentication',
+ 'interface',
+ interface,
+ 'mac',
+ mac_address,
+ 'ip-address',
+ ip_address,
+ ]
+ )
+ self.set(['authentication', 'mode', 'local'])
+ # commit changes
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(allow_no_value=True, delimiters='=', strict=False)
+ conf.read(self._config_file)
+
+ # basic verification
+ self.verify(conf)
+
+ # check local users
+ tmp = cmd(f'sudo cat {self._chap_secrets}')
+ regex = f'{interface}\s+\*\s+{mac_address}\s+{ip_address}'
+ tmp = re.findall(regex, tmp)
+ self.assertTrue(tmp)
+
+ def test_ipoe_server_start_session(self):
+ start_session = 'auto'
+
+ # Configuration of local authentication for PPPoE server
+ self.basic_config()
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(allow_no_value=True, delimiters='=', strict=False)
+ conf.read(self._config_file)
+ # if 'start-session' option is not set the default value is 'dhcp'
+ self.assertIn(f'start=dhcpv4', conf['ipoe']['interface'])
+
+ # change 'start-session' option to 'auto'
+ self.set(['interface', interface, 'start-session', start_session])
+ self.cli_commit()
+
+ # Validate changed configuration values
+ conf = ConfigParser(allow_no_value=True, delimiters='=', strict=False)
+ conf.read(self._config_file)
+ self.assertIn(f'start={start_session}', conf['ipoe']['interface'])
+
+ 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
@@ -269,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 9d72ef78f..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-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
@@ -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)
@@ -117,12 +119,14 @@ class TestServiceLLDP(VyOSUnitTestSHIM.TestCase):
config = read_file(LLDPD_CONF)
self.assertIn(f'configure ports {interface} med location elin "{elin}"', config)
+ # This is the CLI default mode
+ self.assertIn(f'configure ports {interface} lldp status rx-and-tx', config)
self.assertIn(f'configure system interface pattern "{interface}"', config)
def test_06_lldp_snmp(self):
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'])
@@ -134,5 +138,50 @@ class TestServiceLLDP(VyOSUnitTestSHIM.TestCase):
self.cli_delete(['service', 'snmp'])
+ def test_07_lldp_interface_mode(self):
+ interfaces = Section.interfaces('ethernet', vlan=False)
+
+ # set interface mode to 'tx'
+ self.cli_set(base_path + ['interface', 'all'])
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'mode', 'disable'])
+ # commit changes
+ self.cli_commit()
+
+ # verify configuration
+ config = read_file(LLDPD_CONF)
+ for interface in interfaces:
+ self.assertIn(f'configure ports {interface} lldp status disable', config)
+
+ # Change configuration to rx-only
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'mode', 'rx'])
+ # commit changes
+ self.cli_commit()
+ # verify configuration
+ config = read_file(LLDPD_CONF)
+ for interface in interfaces:
+ self.assertIn(f'configure ports {interface} lldp status rx-only', config)
+
+ # Change configuration to tx-only
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'mode', 'tx'])
+ # commit changes
+ self.cli_commit()
+ # verify configuration
+ config = read_file(LLDPD_CONF)
+ for interface in interfaces:
+ self.assertIn(f'configure ports {interface} lldp status tx-only', config)
+
+ # Change configuration to rx-only
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'mode', 'rx-tx'])
+ # commit changes
+ self.cli_commit()
+ # verify configuration
+ config = read_file(LLDPD_CONF)
+ for interface in interfaces:
+ self.assertIn(f'configure ports {interface} lldp status rx-and-tx', config)
+
if __name__ == '__main__':
- unittest.main(verbosity=2)
+ 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 f2fb3b509..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
@@ -21,40 +21,50 @@ from base_vyostest_shim import VyOSUnitTestSHIM
from configparser import ConfigParser
from vyos.configsession import ConfigSessionError
from vyos.utils.process import process_named_running
+from vyos.xml_ref import default_value
base_path = ['service', 'mdns', 'repeater']
intf_base = ['interfaces', 'dummy']
config_file = '/run/avahi-daemon/avahi-daemon.conf'
-
+PROCESS_NAME = 'avahi-daemon'
class TestServiceMDNSrepeater(VyOSUnitTestSHIM.TestCase):
- def setUp(self):
- # Start with a clean CLI instance
- self.cli_delete(base_path)
+ @classmethod
+ def setUpClass(cls):
+ super(TestServiceMDNSrepeater, cls).setUpClass()
- # Service required a configured IP address on the interface
- self.cli_set(intf_base + ['dum10', 'address', '192.0.2.1/30'])
- self.cli_set(intf_base + ['dum10', 'ipv6', 'address', 'no-default-link-local'])
- self.cli_set(intf_base + ['dum20', 'address', '192.0.2.5/30'])
- self.cli_set(intf_base + ['dum20', 'address', '2001:db8:0:2::5/64'])
- self.cli_set(intf_base + ['dum30', 'address', '192.0.2.9/30'])
- self.cli_set(intf_base + ['dum30', 'address', '2001:db8:0:2::9/64'])
- self.cli_set(intf_base + ['dum40', 'address', '2001:db8:0:2::11/64'])
- self.cli_commit()
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ cls.cli_set(cls, intf_base + ['dum10', 'address', '192.0.2.1/30'])
+ cls.cli_set(cls, intf_base + ['dum10', 'ipv6', 'address', 'no-default-link-local'])
+ cls.cli_set(cls, intf_base + ['dum20', 'address', '192.0.2.5/30'])
+ cls.cli_set(cls, intf_base + ['dum20', 'address', '2001:db8:0:2::5/64'])
+ cls.cli_set(cls, intf_base + ['dum30', 'address', '192.0.2.9/30'])
+ cls.cli_set(cls, intf_base + ['dum30', 'address', '2001:db8:0:2::9/64'])
+ cls.cli_set(cls, intf_base + ['dum40', 'address', '2001:db8:0:2::11/64'])
+
+ cls.cli_commit(cls)
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, intf_base + ['dum10'])
+ cls.cli_delete(cls, intf_base + ['dum20'])
+ cls.cli_delete(cls, intf_base + ['dum30'])
+ cls.cli_delete(cls, intf_base + ['dum40'])
+
+ cls.cli_commit(cls)
def tearDown(self):
# Check for running process
- self.assertTrue(process_named_running('avahi-daemon'))
-
+ self.assertTrue(process_named_running(PROCESS_NAME))
self.cli_delete(base_path)
- self.cli_delete(intf_base + ['dum10'])
- self.cli_delete(intf_base + ['dum20'])
- self.cli_delete(intf_base + ['dum30'])
- self.cli_delete(intf_base + ['dum40'])
self.cli_commit()
-
# Check that there is no longer a running process
- 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)
@@ -92,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'])
@@ -109,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'])
@@ -130,5 +140,38 @@ class TestServiceMDNSrepeater(VyOSUnitTestSHIM.TestCase):
self.assertEqual(conf['server']['allow-interfaces'], 'dum30, dum40')
self.assertEqual(conf['reflector']['enable-reflector'], 'yes')
+ def test_service_max_cache_entries(self):
+ cli_default_max_cache = default_value(base_path + ['cache-entries'])
+ self.cli_set(base_path)
+
+ # Need at least two interfaces
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['interface', 'dum20'])
+
+ # Need at least two interfaces
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['interface', 'dum30'])
+
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(delimiters='=')
+ conf.read(config_file)
+ self.assertEqual(conf['server']['cache-entries-max'], cli_default_max_cache)
+
+ # Set max cache entries
+ cache_entries = '1234'
+ self.cli_set(base_path + ['cache-entries', cache_entries])
+
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(delimiters='=')
+ conf.read(config_file)
+
+ self.assertEqual(conf['server']['cache-entries-max'], cache_entries)
+
if __name__ == '__main__':
- unittest.main(verbosity=2)
+ 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
new file mode 100644
index 000000000..e36e16b03
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_monitoring_network_event.py
@@ -0,0 +1,67 @@
+#!/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.utils.file import read_json
+
+
+base_path = ['service', 'monitoring', 'network-event']
+
+
+def get_logger_config():
+ return read_json('/run/vyos-network-event-logger.conf')
+
+
+class TestMonitoringNetworkEvent(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestMonitoringNetworkEvent, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+ # always forward to base class
+ super().tearDown()
+
+ def test_network_event_log(self):
+ expected_config = {
+ 'event': {
+ 'route': {},
+ 'link': {},
+ 'addr': {},
+ 'neigh': {},
+ 'rule': {},
+ },
+ 'queue_size': '10000'
+ }
+
+ self.cli_set(base_path + ['event', 'route'])
+ self.cli_set(base_path + ['event', 'link'])
+ self.cli_set(base_path + ['event', 'addr'])
+ self.cli_set(base_path + ['event', 'neigh'])
+ self.cli_set(base_path + ['event', 'rule'])
+ self.cli_set(base_path + ['queue-size', '10000'])
+ self.cli_commit()
+ self.assertEqual(expected_config, get_logger_config())
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, 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
new file mode 100755
index 000000000..df27162ef
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_monitoring_prometheus.py
@@ -0,0 +1,163 @@
+#!/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 process_named_running
+from vyos.utils.file import read_file
+
+NODE_EXPORTER_PROCESS_NAME = 'node_exporter'
+FRR_EXPORTER_PROCESS_NAME = 'frr_exporter'
+BLACKBOX_EXPORTER_PROCESS_NAME = 'blackbox_exporter'
+
+base_path = ['service', 'monitoring', 'prometheus']
+listen_if = 'dum3421'
+listen_ip = '192.0.2.1'
+node_exporter_service_file = '/etc/systemd/system/node_exporter.service'
+frr_exporter_service_file = '/etc/systemd/system/frr_exporter.service'
+blackbox_exporter_service_file = '/etc/systemd/system/blackbox_exporter.service'
+
+
+class TestMonitoringPrometheus(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ # call base-classes classmethod
+ super(TestMonitoringPrometheus, cls).setUpClass()
+ # create a test interfaces
+ cls.cli_set(
+ cls, ['interfaces', 'dummy', listen_if, 'address', listen_ip + '/32']
+ )
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, ['interfaces', 'dummy', listen_if])
+ super(TestMonitoringPrometheus, cls).tearDownClass()
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+ self.assertFalse(process_named_running(NODE_EXPORTER_PROCESS_NAME))
+ self.assertFalse(process_named_running(FRR_EXPORTER_PROCESS_NAME))
+ # always forward to base class
+ super().tearDown()
+
+ def test_01_node_exporter(self):
+ self.cli_set(base_path + ['node-exporter', 'listen-address', listen_ip])
+ self.cli_set(base_path + ['node-exporter', 'collectors', 'textfile'])
+
+ # commit changes
+ self.cli_commit()
+
+ file_content = read_file(node_exporter_service_file)
+ self.assertIn(f'{listen_ip}:9100', file_content)
+
+ self.assertTrue(os.path.isdir('/run/node_exporter/collector'))
+ self.assertIn(
+ '--collector.textfile.directory=/run/node_exporter/collector', file_content
+ )
+
+ # Check for running process
+ self.assertTrue(process_named_running(NODE_EXPORTER_PROCESS_NAME))
+
+ def test_02_frr_exporter(self):
+ self.cli_set(base_path + ['frr-exporter', 'listen-address', listen_ip])
+
+ # commit changes
+ self.cli_commit()
+
+ file_content = read_file(frr_exporter_service_file)
+ self.assertIn(f'{listen_ip}:9342', file_content)
+
+ # Check for running process
+ self.assertTrue(process_named_running(FRR_EXPORTER_PROCESS_NAME))
+
+ def test_03_blackbox_exporter(self):
+ self.cli_set(base_path + ['blackbox-exporter', 'listen-address', listen_ip])
+
+ # commit changes
+ self.cli_commit()
+
+ file_content = read_file(blackbox_exporter_service_file)
+ self.assertIn(f'{listen_ip}:9115', file_content)
+
+ # Check for running process
+ self.assertTrue(process_named_running(BLACKBOX_EXPORTER_PROCESS_NAME))
+
+ def test_04_blackbox_exporter_with_config(self):
+ self.cli_set(base_path + ['blackbox-exporter', 'listen-address', listen_ip])
+ self.cli_set(
+ base_path
+ + [
+ 'blackbox-exporter',
+ 'modules',
+ 'dns',
+ 'name',
+ 'dns_ip4',
+ 'preferred-ip-protocol',
+ 'ipv4',
+ ]
+ )
+ self.cli_set(
+ base_path
+ + [
+ 'blackbox-exporter',
+ 'modules',
+ 'dns',
+ 'name',
+ 'dns_ip4',
+ 'query-name',
+ 'vyos.io',
+ ]
+ )
+ self.cli_set(
+ base_path
+ + [
+ 'blackbox-exporter',
+ 'modules',
+ 'dns',
+ 'name',
+ 'dns_ip4',
+ 'query-type',
+ 'A',
+ ]
+ )
+ self.cli_set(
+ base_path
+ + [
+ 'blackbox-exporter',
+ 'modules',
+ 'icmp',
+ 'name',
+ 'icmp_ip6',
+ 'preferred-ip-protocol',
+ 'ipv6',
+ ]
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ file_content = read_file(blackbox_exporter_service_file)
+ self.assertIn(f'{listen_ip}:9115', file_content)
+
+ # Check for running process
+ self.assertTrue(process_named_running(BLACKBOX_EXPORTER_PROCESS_NAME))
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, 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 a60dae0a0..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
@@ -23,6 +23,7 @@ from vyos.utils.file import read_file
PROCESS_NAME = 'zabbix_agent2'
ZABBIX_AGENT_CONF = '/run/zabbix/zabbix-agent2.conf'
+ZABBIX_PSK_FILE = f'/run/zabbix/zabbix-agent2.psk'
base_path = ['service', 'monitoring', 'zabbix-agent']
@@ -30,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'
@@ -82,6 +83,26 @@ class TestZabbixAgent(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'Timeout={timeout}', config)
self.assertIn(f'Hostname={hostname}', config)
+ def test_02_zabbix_agent_psk_auth(self):
+ secret = '8703ce4cb3f51279acba895e1421d69d8a7e2a18546d013d564ad87ac3957f29'
+ self.cli_set(base_path + ['server', '127.0.0.1'])
+ self.cli_set(base_path + ['authentication', 'mode', 'pre-shared-secret'])
+ self.cli_set(base_path + ['authentication', 'psk', 'id', 'smoke_test'])
+ self.cli_set(base_path + ['authentication', 'psk', 'secret', secret])
+ self.cli_commit()
+
+ config = read_file(ZABBIX_AGENT_CONF)
+ self.assertIn('TLSConnect=psk', config)
+ self.assertIn('TLSAccept=psk', config)
+ self.assertIn('TLSPSKIdentity=smoke_test', config)
+ self.assertIn(f'TLSPSKFile={ZABBIX_PSK_FILE}', config)
+ self.assertEqual(secret, read_file(ZABBIX_PSK_FILE))
+
+ secret = '8703ce4cb3f51279acba895e1421d69d8a7e2a18546d013d564ad87ac3957f88'
+ self.cli_set(base_path + ['authentication', 'psk', 'secret', secret])
+ self.cli_commit()
+ self.assertEqual(secret, read_file(ZABBIX_PSK_FILE))
+
if __name__ == '__main__':
- unittest.main(verbosity=2)
+ 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 ae45fe2f4..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,8 +19,10 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
+from vyos.utils.file import read_file
from vyos.utils.process import cmd
from vyos.utils.process import process_named_running
+from vyos.xml_ref import default_value
PROCESS_NAME = 'chronyd'
NTP_CONF = '/run/chrony/chrony.conf'
@@ -37,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
@@ -62,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)
@@ -77,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']
@@ -87,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)
@@ -119,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)
@@ -150,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:
@@ -165,5 +169,96 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'maxslewrate 1000', config)
self.assertIn(f'smoothtime 400 0.001024 leaponly', config)
+ def test_interleave_option(self):
+ # "interleave" option differs from some others in that the
+ # name is not a 1:1 mapping from VyOS config
+ servers = ['192.0.2.1', '192.0.2.2']
+ options = ['prefer']
+
+ for server in servers:
+ for option in options:
+ self.cli_set(base_path + ['server', server, option])
+ self.cli_set(base_path + ['server', server, 'interleave'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check generated configuration
+ 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)
+ self.assertIn('clientloglimit 1048576', config)
+ self.assertIn('rtcsync', config)
+ self.assertIn('makestep 1.0 3', config)
+ self.assertIn('leapsectz right/UTC', config)
+
+ for server in servers:
+ self.assertIn(f'server {server} iburst ' + ' '.join(options) + ' xleave', config)
+
+ def test_offload_timestamp_default(self):
+ # Test offloading of NIC timestamp
+ servers = ['192.0.2.1', '192.0.2.2']
+ ptp_port = '8319'
+
+ for server in servers:
+ self.cli_set(base_path + ['server', server, 'ptp'])
+
+ self.cli_set(base_path + ['ptp', 'port', ptp_port])
+ self.cli_set(base_path + ['timestamp', 'interface', 'all'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check generated configuration
+ 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)
+ self.assertIn('clientloglimit 1048576', config)
+ self.assertIn('rtcsync', config)
+ self.assertIn('makestep 1.0 3', config)
+ self.assertIn('leapsectz right/UTC', config)
+
+ for server in servers:
+ self.assertIn(f'server {server} iburst port {ptp_port}', config)
+
+ self.assertIn('hwtimestamp *', config)
+
+ def test_ptp_transport(self):
+ # Test offloading of NIC timestamp
+ servers = ['192.0.2.1', '192.0.2.2']
+ options = ['prefer']
+
+ default_ptp_port = default_value(base_path + ['ptp', 'port'])
+
+ for server in servers:
+ for option in options:
+ self.cli_set(base_path + ['server', server, option])
+ self.cli_set(base_path + ['server', server, 'ptp'])
+
+ # commit changes (expected to fail)
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # add the required top-level option and commit
+ self.cli_set(base_path + ['ptp'])
+ self.cli_commit()
+
+ # Check generated configuration
+ 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)
+ self.assertIn('clientloglimit 1048576', config)
+ self.assertIn('rtcsync', config)
+ self.assertIn('makestep 1.0 3', config)
+ self.assertIn('leapsectz right/UTC', config)
+
+ for server in servers:
+ self.assertIn(f'server {server} iburst ' + ' '.join(options) + f' port {default_ptp_port}', config)
+
+ self.assertIn(f'ptpport {default_ptp_port}', config)
+
if __name__ == '__main__':
- unittest.main(verbosity=2)
+ 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 d8e325eee..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,30 +19,122 @@ 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'
SSHD_CONF = '/run/sshd/sshd_config'
base_path = ['service', 'ssh']
+pki_path = ['pki']
key_rsa = '/etc/ssh/ssh_host_rsa_key'
key_dsa = '/etc/ssh/ssh_host_dsa_key'
key_ed25519 = '/etc/ssh/ssh_host_ed25519_key'
+trusted_user_ca = 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=
+"""
+
+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
def setUpClass(cls):
@@ -70,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
@@ -79,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)
@@ -98,27 +192,27 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase):
# Check configured port
port = get_config_value('Port')[0]
- self.assertTrue("1234" in port)
+ self.assertTrue('1234' in port)
# Check DNS usage
dns = get_config_value('UseDNS')[0]
- self.assertTrue("no" in dns)
+ self.assertTrue('no' in dns)
# Check PasswordAuthentication
pwd = get_config_value('PasswordAuthentication')[0]
- self.assertTrue("no" in pwd)
+ self.assertTrue('no' in pwd)
# Check loglevel
loglevel = get_config_value('LogLevel')[0]
- self.assertTrue("VERBOSE" in loglevel)
+ self.assertTrue('VERBOSE' in loglevel)
# Check listen address
address = get_config_value('ListenAddress')[0]
- self.assertTrue("127.0.0.1" in address)
+ self.assertTrue('127.0.0.1' in address)
# Check keepalive
keepalive = get_config_value('ClientAliveInterval')[0]
- self.assertTrue("100" in keepalive)
+ self.assertTrue('100' in keepalive)
def test_ssh_multiple_listen_addresses(self):
# Check if SSH service can be configured and runs with multiple
@@ -191,13 +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()
@@ -210,13 +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):
@@ -250,7 +344,7 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase):
sshguard_lines = [
f'THRESHOLD={threshold}',
f'BLOCK_TIME={block_time}',
- f'DETECTION_TIME={detect_time}'
+ f'DETECTION_TIME={detect_time}',
]
tmp_sshguard_conf = read_file(SSHGUARD_CONFIG)
@@ -268,18 +362,22 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase):
self.assertFalse(process_named_running(SSHGUARD_PROCESS))
-
# Network Device Collaborative Protection Profile
def test_ssh_ndcpp(self):
ciphers = ['aes128-cbc', 'aes128-ctr', 'aes256-cbc', 'aes256-ctr']
host_key_algs = ['sk-ssh-ed25519@openssh.com', 'ssh-rsa', 'ssh-ed25519']
- kexes = ['diffie-hellman-group14-sha1', 'ecdh-sha2-nistp256', 'ecdh-sha2-nistp384', 'ecdh-sha2-nistp521']
+ kexes = [
+ 'diffie-hellman-group14-sha1',
+ 'ecdh-sha2-nistp256',
+ 'ecdh-sha2-nistp384',
+ 'ecdh-sha2-nistp521',
+ ]
macs = ['hmac-sha1', 'hmac-sha2-256', 'hmac-sha2-512']
rekey_time = '60'
rekey_data = '1024'
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:
@@ -293,22 +391,29 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase):
# commit changes
self.cli_commit()
- ssh_lines = ['Ciphers aes128-cbc,aes128-ctr,aes256-cbc,aes256-ctr',
- 'HostKeyAlgorithms sk-ssh-ed25519@openssh.com,ssh-rsa,ssh-ed25519',
- 'MACs hmac-sha1,hmac-sha2-256,hmac-sha2-512',
- 'KexAlgorithms diffie-hellman-group14-sha1,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521',
- 'RekeyLimit 1024M 60M'
- ]
+ ssh_lines = [
+ 'Ciphers aes128-cbc,aes128-ctr,aes256-cbc,aes256-ctr',
+ 'HostKeyAlgorithms sk-ssh-ed25519@openssh.com,ssh-rsa,ssh-ed25519',
+ 'MACs hmac-sha1,hmac-sha2-256,hmac-sha2-512',
+ 'KexAlgorithms diffie-hellman-group14-sha1,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521',
+ 'RekeyLimit 1024M 60M',
+ ]
tmp_sshd_conf = read_file(SSHD_CONF)
for line in ssh_lines:
self.assertIn(line, tmp_sshd_conf)
def test_ssh_pubkey_accepted_algorithm(self):
- algs = ['ssh-ed25519', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384',
- 'ecdsa-sha2-nistp521', 'ssh-dss', 'ssh-rsa', 'rsa-sha2-256',
- 'rsa-sha2-512'
- ]
+ algs = [
+ 'ssh-ed25519',
+ 'ecdsa-sha2-nistp256',
+ 'ecdsa-sha2-nistp384',
+ 'ecdsa-sha2-nistp521',
+ 'ssh-dss',
+ 'ssh-rsa',
+ 'rsa-sha2-256',
+ 'rsa-sha2-512',
+ ]
expected = 'PubkeyAcceptedAlgorithms '
for alg in algs:
@@ -320,6 +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(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()
+
+ 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()
+
+ # 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)
+
+ # 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(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_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 2b3f6d21c..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-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
@@ -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'
@@ -297,6 +299,22 @@ class TestServiceWebProxy(VyOSUnitTestSHIM.TestCase):
# Check for running process
self.assertTrue(process_named_running(PROCESS_NAME))
+ def test_06_nocache_domain_proxy(self):
+ domains_nocache = ['test1.net', 'test2.net']
+ self.cli_set(base_path + ['listen-address', listen_ip])
+ for domain in domains_nocache:
+ self.cli_set(base_path + ['domain-noncache', domain])
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(PROXY_CONF)
+
+ for domain in domains_nocache:
+ self.assertIn(f'acl NOCACHE dstdomain {domain}', config)
+ self.assertIn(f'no_cache deny NOCACHE', config)
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
if __name__ == '__main__':
- unittest.main(verbosity=2)
+ 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 515134220..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,255 +77,222 @@ 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)
-
- def test_sflow(self):
- sampling_rate = '4000'
- source_address = '192.0.2.1'
- dummy_if = 'dum3841'
- agent_address = '192.0.2.2'
-
- sflow_server = {
- '1.2.3.4' : { },
- '5.6.7.8' : { 'port' : '6000' },
- }
+ self._assert_ingress_interfaces(Section.interfaces('ethernet'))
+ self._assert_egress_interfaces([])
- self.cli_set(['interfaces', 'dummy', dummy_if, 'address', agent_address + '/32'])
- self.cli_set(['interfaces', 'dummy', dummy_if, 'address', source_address + '/32'])
- self.cli_set(base_path + ['disable-imt'])
+ 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'
+ dummy_if = 'dum3842'
+ agent_address = '192.0.2.10'
+ version = '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'},
+ '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},
+ }
+ # 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']
+ )
- # 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])
+ 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', 'version', version])
+ 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 sFlow or NetFlow protocol, or not
- # set "disable-imt" for flow-accounting
+ # You need to configure at least one netflow server
with self.assertRaises(ConfigSessionError):
self.cli_commit()
- self.cli_set(base_path + ['sflow', 'agent-address', agent_address])
- self.cli_set(base_path + ['sflow', 'sampling-rate', sampling_rate])
- self.cli_set(base_path + ['sflow', 'source-address', source_address])
- for server, server_config in sflow_server.items():
- self.cli_set(base_path + ['sflow', 'server', server])
+ for server, server_config in netflow_server.items():
+ self.cli_set(base_path + ['netflow', 'server', server])
if 'port' in server_config:
- self.cli_set(base_path + ['sflow', 'server', server, 'port', server_config['port']])
+ 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)
- # when 'disable-imt' is not configured on the CLI it must be present
- self.assertNotIn(f'imt_path: /tmp/uacctd.pipe', uacctd)
- self.assertNotIn(f'imt_mem_pools_number: 169', uacctd)
- self.assertNotIn(f'plugins: memory', uacctd)
+ 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 sflow_server.items():
- plugin_name = server.replace('.', '-')
- if 'port' in server_config:
- self.assertIn(f'sfprobe_receiver[sf_{plugin_name}]: {server}', uacctd)
- else:
- self.assertIn(f'sfprobe_receiver[sf_{plugin_name}]: {server}:6343', uacctd)
+ # Test module reload with new parameters
+ engine_id = '73'
+ self.cli_set(base_path + ['netflow', 'engine-id', engine_id])
+ self.cli_commit()
+
+ module_data = get_module_data(module_name)
- self.assertIn(f'sfprobe_agentip[sf_{plugin_name}]: {agent_address}', uacctd)
- self.assertIn(f'sampling_rate[sf_{plugin_name}]: {sampling_rate}', uacctd)
- self.assertIn(f'sfprobe_source_ip[sf_{plugin_name}]: {source_address}', uacctd)
+ self.assertEqual(engine_id, module_data['parameters']['engine_id'])
self.cli_delete(['interfaces', 'dummy', dummy_if])
- def test_sflow_ipv6(self):
- sampling_rate = '100'
- sflow_server = {
- '2001:db8::1' : { },
- '2001:db8::2' : { 'port' : '6000' },
- }
- self.cli_set(base_path + ['disable-imt'])
+ def test_iptables(self):
+ netflow_server = '11.22.33.44'
+ self.cli_set(base_path + ['netflow', 'server', netflow_server])
- # 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])
+ 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'])
- # You need to configure at least one sFlow or NetFlow protocol, or not
- # set "disable-imt" for flow-accounting
- with self.assertRaises(ConfigSessionError):
- self.cli_commit()
-
- self.cli_set(base_path + ['sflow', 'sampling-rate', sampling_rate])
- for server, server_config in sflow_server.items():
- self.cli_set(base_path + ['sflow', 'server', server])
- if 'port' in server_config:
- self.cli_set(base_path + ['sflow', 'server', server, 'port', server_config['port']])
-
- # commit changes
+ # * 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([])
- uacctd = read_file(uacctd_conf)
-
- # when 'disable-imt' is not configured on the CLI it must be present
- self.assertNotIn(f'imt_path: /tmp/uacctd.pipe', uacctd)
- self.assertNotIn(f'imt_mem_pools_number: 169', uacctd)
- self.assertNotIn(f'plugins: memory', uacctd)
+ # * 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([])
- for server, server_config in sflow_server.items():
- tmp_srv = server
- tmp_srv = tmp_srv.replace(':', '-')
+ # * 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([])
- if 'port' in server_config:
- self.assertIn(f'sfprobe_receiver[sf_{tmp_srv}]: {bracketize_ipv6(server)}', uacctd)
- else:
- self.assertIn(f'sfprobe_receiver[sf_{tmp_srv}]: {bracketize_ipv6(server)}:6343', uacctd)
- self.assertIn(f'sampling_rate[sf_{tmp_srv}]: {sampling_rate}', uacctd)
+ # * 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_netflow(self):
- engine_id = '33'
- max_flows = '667'
+ 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'
- 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'
-
- netflow_server = {
- '11.22.33.44' : { },
- '55.66.77.88' : { 'port' : '6000' },
- '2001:db8::1' : { },
- }
-
- self.cli_set(['interfaces', 'dummy', dummy_if, 'address', agent_address + '/32'])
- self.cli_set(['interfaces', 'dummy', dummy_if, 'address', source_address + '/32'])
-
- for interface in Section.interfaces('ethernet'):
- self.cli_set(base_path + ['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])
-
- # You need to configure at least one netflow server
- with self.assertRaises(ConfigSessionError):
- self.cli_commit()
+ for interface in Section.interfaces('ethernet'):
+ self.cli_set(base_path + ['netflow', 'interface', interface])
- for server, server_config in netflow_server.items():
- 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']])
+ netflow_server = '11.22.33.44'
+ self.cli_set(base_path + ['netflow', 'server', netflow_server])
- # commit changes
+ # commit changes, this time should work
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)
+ if 'sampler' not in module_data['parameters']:
+ self.skipTest("ipt_NETFLOW has no sampler parameter")
- for server, server_config in netflow_server.items():
- tmp_srv = server
- tmp_srv = tmp_srv.replace('.', '-')
- tmp_srv = tmp_srv.replace(':', '-')
+ self.assertEqual(
+ f'random:{sampling_rate}', module_data['parameters']['sampler']
+ )
- 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)
- 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)
+ def test_netflow_v5(self):
+ version = '5'
+ netflow_server = '203.0.113.27'
- 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)
+ 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()
- self.cli_delete(['interfaces', 'dummy', dummy_if])
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 5b0090237..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
@@ -18,72 +18,83 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
-from vyos.utils.file import read_file
+from vyos.utils.system import sysctl_read
+from vyos.xml_ref import default_value
base_path = ['system', 'ip']
class TestSystemIP(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestSystemIP, cls).setUpClass()
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
def tearDown(self):
self.cli_delete(base_path)
self.cli_commit()
+ # 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
- all_forwarding = '/proc/sys/net/ipv4/conf/all/forwarding'
- self.assertEqual(read_file(all_forwarding), '1')
+ self.assertEqual(sysctl_read(['net', 'ipv4', 'conf', 'all', 'forwarding']), '1')
self.cli_set(base_path + ['disable-forwarding'])
self.cli_commit()
- self.assertEqual(read_file(all_forwarding), '0')
+ 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()
+ self.assertNotIn('no ip forwarding', frrconfig)
def test_system_ip_multipath(self):
# Test IPv4 multipathing options, options default to off -> '0'
- use_neigh = '/proc/sys/net/ipv4/fib_multipath_use_neigh'
- hash_policy = '/proc/sys/net/ipv4/fib_multipath_hash_policy'
-
- self.assertEqual(read_file(use_neigh), '0')
- self.assertEqual(read_file(hash_policy), '0')
+ self.assertEqual(sysctl_read(['net', 'ipv4', 'fib_multipath_use_neigh']), '0')
+ self.assertEqual(sysctl_read(['net', 'ipv4', 'fib_multipath_hash_policy']), '0')
self.cli_set(base_path + ['multipath', 'ignore-unreachable-nexthops'])
self.cli_set(base_path + ['multipath', 'layer4-hashing'])
self.cli_commit()
- self.assertEqual(read_file(use_neigh), '1')
- self.assertEqual(read_file(hash_policy), '1')
+ self.assertEqual(sysctl_read(['net', 'ipv4', 'fib_multipath_use_neigh']), '1')
+ self.assertEqual(sysctl_read(['net', 'ipv4', 'fib_multipath_hash_policy']), '1')
def test_system_ip_arp_table_size(self):
- # Maximum number of entries to keep in the ARP cache, the
- # default is 8k
+ cli_default = int(default_value(base_path + ['arp', 'table-size']))
+ def _verify_gc_thres(table_size):
+ self.assertEqual(sysctl_read(['net', 'ipv4', 'neigh', 'default', 'gc_thresh3']), str(table_size))
+ self.assertEqual(sysctl_read(['net', 'ipv4', 'neigh', 'default', 'gc_thresh2']), str(table_size // 2))
+ self.assertEqual(sysctl_read(['net', 'ipv4', 'neigh', 'default', 'gc_thresh1']), str(table_size // 8))
- gc_thresh3 = '/proc/sys/net/ipv4/neigh/default/gc_thresh3'
- gc_thresh2 = '/proc/sys/net/ipv4/neigh/default/gc_thresh2'
- gc_thresh1 = '/proc/sys/net/ipv4/neigh/default/gc_thresh1'
- self.assertEqual(read_file(gc_thresh3), '8192')
- self.assertEqual(read_file(gc_thresh2), '4096')
- self.assertEqual(read_file(gc_thresh1), '1024')
+ _verify_gc_thres(cli_default)
for size in [1024, 2048, 4096, 8192, 16384, 32768]:
self.cli_set(base_path + ['arp', 'table-size', str(size)])
self.cli_commit()
-
- self.assertEqual(read_file(gc_thresh3), str(size))
- self.assertEqual(read_file(gc_thresh2), str(size // 2))
- self.assertEqual(read_file(gc_thresh1), str(size // 8))
+ _verify_gc_thres(size)
def test_system_ip_protocol_route_map(self):
- protocols = ['any', 'babel', 'bgp', 'connected', 'eigrp', 'isis',
- '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='', daemon='zebra')
+ 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)
@@ -94,7 +105,7 @@ class TestSystemIP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify route-map properly applied to FRR
- frrconfig = self.getFRRconfig('ip protocol', end='', daemon='zebra')
+ frrconfig = self.getFRRconfig('ip protocol', stop_section='^end')
self.assertNotIn(f'ip protocol', frrconfig)
def test_system_ip_protocol_non_existing_route_map(self):
@@ -113,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='', daemon='zebra')
+ 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='', daemon='zebra')
+ 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 0c77c1dd4..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
@@ -19,33 +19,45 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
-from vyos.utils.file import read_file
+from vyos.utils.system import sysctl_read
+from vyos.xml_ref import default_value
base_path = ['system', 'ipv6']
-file_forwarding = '/proc/sys/net/ipv6/conf/all/forwarding'
-file_disable = '/proc/sys/net/ipv6/conf/all/disable_ipv6'
-file_dad = '/proc/sys/net/ipv6/conf/all/accept_dad'
-file_multipath = '/proc/sys/net/ipv6/fib_multipath_hash_policy'
-
class TestSystemIPv6(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestSystemIPv6, cls).setUpClass()
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
def tearDown(self):
self.cli_delete(base_path)
self.cli_commit()
+ # 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(read_file(file_forwarding), '1')
+ self.assertEqual(sysctl_read(['net', 'ipv6', 'conf', 'all', 'forwarding']), '1')
self.cli_set(base_path + ['disable-forwarding'])
self.cli_commit()
+ self.assertEqual(sysctl_read(['net', 'ipv6', 'conf', 'all', 'forwarding']), '0')
+ frrconfig = self.getFRRconfig()
+ self.assertIn('no ipv6 forwarding', frrconfig)
- self.assertEqual(read_file(file_forwarding), '0')
+ self.cli_delete(base_path + ['disable-forwarding'])
+ self.cli_commit()
+ self.assertEqual(sysctl_read(['net', 'ipv6', 'conf', 'all', 'forwarding']), '1')
+ frrconfig = self.getFRRconfig()
+ self.assertNotIn('no ipv6 forwarding', frrconfig)
def test_system_ipv6_strict_dad(self):
# This defaults to 1
- self.assertEqual(read_file(file_dad), '1')
+ self.assertEqual(sysctl_read(['net', 'ipv6', 'conf', 'all', 'accept_dad']), '1')
# Do not assign any IPv6 address on interfaces, this requires a reboot
# which can not be tested, but we can read the config file :)
@@ -53,11 +65,11 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify configuration file
- self.assertEqual(read_file(file_dad), '2')
+ self.assertEqual(sysctl_read(['net', 'ipv6', 'conf', 'all', 'accept_dad']), '2')
def test_system_ipv6_multipath(self):
# This defaults to 0
- self.assertEqual(read_file(file_multipath), '0')
+ self.assertEqual(sysctl_read(['net', 'ipv6', 'fib_multipath_hash_policy']), '0')
# Do not assign any IPv6 address on interfaces, this requires a reboot
# which can not be tested, but we can read the config file :)
@@ -65,30 +77,27 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify configuration file
- self.assertEqual(read_file(file_multipath), '1')
+ self.assertEqual(sysctl_read(['net', 'ipv6', 'fib_multipath_hash_policy']), '1')
def test_system_ipv6_neighbor_table_size(self):
# Maximum number of entries to keep in the ARP cache, the
# default is 8192
+ cli_default = int(default_value(base_path + ['neighbor', 'table-size']))
- gc_thresh3 = '/proc/sys/net/ipv6/neigh/default/gc_thresh3'
- gc_thresh2 = '/proc/sys/net/ipv6/neigh/default/gc_thresh2'
- gc_thresh1 = '/proc/sys/net/ipv6/neigh/default/gc_thresh1'
- self.assertEqual(read_file(gc_thresh3), '8192')
- self.assertEqual(read_file(gc_thresh2), '4096')
- self.assertEqual(read_file(gc_thresh1), '1024')
+ def _verify_gc_thres(table_size):
+ self.assertEqual(sysctl_read(['net', 'ipv6', 'neigh', 'default', 'gc_thresh3']), str(table_size))
+ self.assertEqual(sysctl_read(['net', 'ipv6', 'neigh', 'default', 'gc_thresh2']), str(table_size // 2))
+ self.assertEqual(sysctl_read(['net', 'ipv6', 'neigh', 'default', 'gc_thresh1']), str(table_size // 8))
+
+ _verify_gc_thres(cli_default)
for size in [1024, 2048, 4096, 8192, 16384, 32768]:
self.cli_set(base_path + ['neighbor', 'table-size', str(size)])
self.cli_commit()
-
- self.assertEqual(read_file(gc_thresh3), str(size))
- self.assertEqual(read_file(gc_thresh2), str(size // 2))
- self.assertEqual(read_file(gc_thresh1), str(size // 8))
+ _verify_gc_thres(size)
def test_system_ipv6_protocol_route_map(self):
- protocols = ['any', 'babel', 'bgp', 'connected', 'isis',
- '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')
@@ -99,7 +108,7 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify route-map properly applied to FRR
- frrconfig = self.getFRRconfig('ipv6 protocol', end='', daemon='zebra')
+ frrconfig = self.getFRRconfig('ipv6 protocol', end_marker='', stop_section='^end')
for protocol in protocols:
# VyOS and FRR use a different name for OSPFv3 (IPv6)
if protocol == 'ospfv3':
@@ -113,7 +122,7 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify route-map properly applied to FRR
- frrconfig = self.getFRRconfig('ipv6 protocol', end='', daemon='zebra')
+ frrconfig = self.getFRRconfig('ipv6 protocol', stop_section='^end')
self.assertNotIn(f'ipv6 protocol', frrconfig)
def test_system_ipv6_protocol_non_existing_route_map(self):
@@ -132,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='', daemon='zebra')
+ 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='', daemon='zebra')
+ 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 28abba012..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-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,23 +14,38 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import os
import re
import unittest
+import jinja2
+import secrets
+import string
+import paramiko
+import shutil
from base_vyostest_shim import VyOSUnitTestSHIM
from gzip import GzipFile
-from subprocess import Popen, PIPE
-from pwd import getpwall
+from subprocess import Popen
+from subprocess import PIPE
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
from vyos.template import inc_ip
+from vyos.template import is_ipv6
+from vyos.xml_ref import default_value
base_path = ['system', 'login']
users = ['vyos1', 'vyos-roxx123', 'VyOS-123_super.Nice']
+weak_passwd_user = ['test_user', 'passWord1']
+
+ssh_test_command = '/opt/vyatta/bin/vyatta-op-cmd-wrapper show version'
ssh_pubkey = """
AAAAB3NzaC1yc2EAAAADAQABAAABgQD0NuhUOEtMIKnUVFIHoFatqX/c4mjerXyF
@@ -44,6 +59,71 @@ pHJz8umqkxy3hfw0K7BRFtjWd63sbOP8Q/SDV7LPaIfIxenA9zv2rY7y+AIqTmSr
TTSb0X1zPGxPIRFy5GoGtO9Mm5h4OZk=
"""
+tac_image = 'docker.io/lfkeitel/tacacs_plus:alpine'
+tac_image_path = '/usr/share/vyos/tacplus-alpine.tar'
+TAC_PLUS_TMPL_SRC = """
+id = spawnd {
+ debug redirect = /dev/stdout
+ listen = { port = 49 }
+ spawn = {
+ instances min = 1
+ instances max = 10
+ }
+ background = no
+}
+
+id = tac_plus {
+ debug = ALL
+ log = stdout {
+ destination = /dev/stdout
+ }
+ authorization log group = yes
+ authentication log = stdout
+ authorization log = stdout
+ accounting log = stdout
+
+ host = smoketest {
+ address = {{ source_address }}/32
+ enable = clear enable
+ key = {{ tacacs_secret }}
+ }
+
+ group = admin {
+ default service = permit
+ enable = permit
+ service = shell {
+ default command = permit
+ default attribute = permit
+ set priv-lvl = 15
+ }
+ }
+
+ user = {{ username }} {
+ password = clear {{ password }}
+ member = admin
+ }
+}
+
+"""
+
+radius_image = 'docker.io/dchidell/radius-web:latest'
+radius_image_path = '/usr/share/vyos/radius-latest.tar'
+RADIUS_CLIENTS_TMPL_SRC = """
+client SMOKETEST {
+ secret = {{ radius_key }}
+ nastype = other
+ ipaddr = {{ source_address }}
+}
+
+"""
+RADIUS_USERS_TMPL_SRC = """
+# User configuration
+{{ username }} Cleartext-Password := "{{ password }}"
+ Service-Type = NAS-Prompt-User,
+ Cisco-AVPair = "shell:priv-lvl=15"
+
+"""
+
class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
@classmethod
def setUpClass(cls):
@@ -54,6 +134,37 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
cls.cli_delete(cls, base_path + ['radius'])
cls.cli_delete(cls, base_path + ['tacacs'])
+ # Load images for smoketest provided in vyos-1x-smoketest
+ if not os.path.exists(tac_image_path):
+ cls.fail(cls, f'{tac_image} image not available')
+ cmd(f'sudo podman load -i {tac_image_path}')
+
+ if not os.path.exists(radius_image_path):
+ cls.fail(cls, f'{radius_image} image not available')
+ cmd(f'sudo podman load -i {radius_image_path}')
+
+ cls.ssh_test_command_result = cls.op_mode(cls, ['show', 'version'])
+
+ # Dynamically start SSH service if it's not running
+ config = ConfigTreeQuery()
+ cls.is_sshd_pre_test = config.exists(['service', 'sshd'])
+ if not cls.is_sshd_pre_test:
+ # Start SSH service
+ cls.cli_set(cls, ['service', 'ssh'])
+
+ @classmethod
+ def tearDownClass(cls):
+ # Stop SSH service - if it was not running before starting the test
+ if not cls.is_sshd_pre_test:
+ cls.cli_set(cls, ['service', 'ssh'])
+ cls.cli_commit(cls)
+
+ super(TestSystemLogin, cls).tearDownClass()
+
+ # Cleanup container images
+ cmd(f'sudo podman image rm -f {tac_image}')
+ cmd(f'sudo podman image rm -f {radius_image}')
+
def tearDown(self):
# Delete individual users from configuration
for user in users:
@@ -65,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
@@ -83,29 +196,28 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
self.cli_delete(base_path + ['user', system_user])
def test_system_login_user(self):
- # Check if user can be created and we can SSH to localhost
- self.cli_set(['service', 'ssh', 'port', '22'])
-
for user in users:
- name = "VyOS Roxx " + user
- home_dir = "/tmp/" + user
+ name = f'VyOS Roxx {user}'
+ passwd = f'{user}-pSWd-t3st'
+ home_dir = f'/tmp/smoketest/{user}'
- self.cli_set(base_path + ['user', user, 'authentication', 'plaintext-password', user])
- self.cli_set(base_path + ['user', user, 'full-name', 'VyOS Roxx'])
+ self.cli_set(base_path + ['user', user, 'authentication', 'plaintext-password', passwd])
+ self.cli_set(base_path + ['user', user, 'full-name', name])
self.cli_set(base_path + ['user', user, 'home-directory', home_dir])
self.cli_commit()
for user in users:
+ passwd = f'{user}-pSWd-t3st'
tmp = ['su','-', user]
proc = Popen(tmp, stdin=PIPE, stdout=PIPE, stderr=PIPE)
- tmp = "{}\nuname -a".format(user)
+ tmp = f'{passwd}\nuname -a'
proc.stdin.write(tmp.encode())
proc.stdin.flush()
(stdout, stderr) = proc.communicate()
# stdout is something like this:
- # b'Linux LR1.wue3 5.10.61-amd64-vyos #1 SMP Fri Aug 27 08:55:46 UTC 2021 x86_64 GNU/Linux\n'
+ # b'Linux vyos 6.6.66-vyos 6.6.66-vyos #1 SMP Mon Dec 30 19:05:15 UTC 2024 x86_64 GNU/Linux\n'
self.assertTrue(len(stdout) > 40)
locked_user = users[0]
@@ -123,6 +235,24 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
tmp = cmd(f'sudo passwd -S {locked_user}')
self.assertIn(f'{locked_user} P ', tmp)
+ def test_system_login_weak_password_warning(self):
+ username = weak_passwd_user[0]
+ self.cli_set(base_path + [
+ '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.cli_delete(base_path + ['user', weak_passwd_user[0]])
def test_system_login_otp(self):
otp_user = 'otp-test_user'
@@ -172,17 +302,71 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'{option}=y', kernel_config)
def test_system_login_radius_ipv4(self):
- # Verify generated RADIUS configuration files
+ radius_servers = ['100.64.0.4', '100.64.0.5']
+ radius_source = '100.64.0.1'
+ self._system_login_radius_test_helper(radius_servers, radius_source)
+
+ def test_system_login_radius_ipv6(self):
+ radius_servers = ['2001:db8::4', '2001:db8::5']
+ radius_source = '2001:db8::1'
+ self._system_login_radius_test_helper(radius_servers, radius_source)
- radius_key = 'VyOSsecretVyOS'
- radius_server = '172.16.100.10'
- radius_source = '127.0.0.1'
- radius_port = '2000'
- radius_timeout = '1'
+ def _system_login_radius_test_helper(self, radius_servers: list, radius_source: str):
+ # Verify generated RADIUS configuration files
+ radius_key = ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(10))
+
+ default_port = default_value(base_path + ['radius', 'server', radius_servers[0], 'port'])
+ default_timeout = default_value(base_path + ['radius', 'server', radius_servers[0], 'timeout'])
+
+ dummy_if = 'dum12760'
+
+ # Load container image for FreeRADIUS server
+ radius_config = '/tmp/smoketest-radius-server'
+ radius_container_path = ['container', 'name', 'radius-1']
+
+ # Generate random string with 10 digits
+ username = 'radius-admin'
+ password = ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(10))
+ radius_source_mask = '32'
+ if is_ipv6(radius_source):
+ radius_source_mask = '128'
+ radius_test_user = {
+ 'username' : username,
+ 'password' : password,
+ 'radius_key' : radius_key,
+ 'source_address' : f'{radius_source}/{radius_source_mask}'
+ }
+
+ tmpl = jinja2.Template(RADIUS_CLIENTS_TMPL_SRC)
+ write_file(f'{radius_config}/clients.cfg', tmpl.render(radius_test_user))
+
+ tmpl = jinja2.Template(RADIUS_USERS_TMPL_SRC)
+ write_file(f'{radius_config}/users', tmpl.render(radius_test_user))
+
+ # Start tac_plus container
+ self.cli_set(radius_container_path + ['allow-host-networks'])
+ self.cli_set(radius_container_path + ['image', radius_image])
+ self.cli_set(radius_container_path + ['volume', 'clients', 'destination', '/etc/raddb/clients.conf'])
+ self.cli_set(radius_container_path + ['volume', 'clients', 'mode', 'ro'])
+ self.cli_set(radius_container_path + ['volume', 'clients', 'source', f'{radius_config}/clients.cfg'])
+ self.cli_set(radius_container_path + ['volume', 'users', 'destination', '/etc/raddb/users'])
+ self.cli_set(radius_container_path + ['volume', 'users', 'mode', 'ro'])
+ self.cli_set(radius_container_path + ['volume', 'users', 'source', f'{radius_config}/users'])
+
+ # Start container
+ self.cli_commit()
- self.cli_set(base_path + ['radius', 'server', radius_server, 'key', radius_key])
- self.cli_set(base_path + ['radius', 'server', radius_server, 'port', radius_port])
- self.cli_set(base_path + ['radius', 'server', radius_server, 'timeout', radius_timeout])
+ # Deinfine RADIUS servers
+ for radius_server in radius_servers:
+ # Use this system as "remote" RADIUS server
+ dummy_address_mask = '32'
+ if is_ipv6(radius_server):
+ dummy_address_mask = '128'
+ self.cli_set(['interfaces', 'dummy', dummy_if, 'address', f'{radius_server}/{dummy_address_mask}'])
+ self.cli_set(base_path + ['radius', 'server', radius_server, 'key', radius_key])
+
+ # Define RADIUS traffic source address
+ self.cli_set(['interfaces', 'dummy', dummy_if, 'address', f'{radius_source}/{radius_source_mask}'])
self.cli_set(base_path + ['radius', 'source-address', radius_source])
self.cli_set(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)])
@@ -195,10 +379,13 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
# this file must be read with higher permissions
pam_radius_auth_conf = cmd('sudo cat /etc/pam_radius_auth.conf')
- tmp = re.findall(r'\n?{}:{}\s+{}\s+{}\s+{}'.format(radius_server,
- radius_port, radius_key, radius_timeout,
- radius_source), pam_radius_auth_conf)
- self.assertTrue(tmp)
+
+ for radius_server in radius_servers:
+ if is_ipv6(radius_server):
+ # it is essential to escape the [] brackets when searching with a regex
+ radius_server = rf'\[{radius_server}\]'
+ tmp = re.findall(rf'\n?{radius_server}:{default_port}\s+{radius_key}\s+{default_timeout}\s+{radius_source}', pam_radius_auth_conf)
+ self.assertTrue(tmp)
# required, static options
self.assertIn('priv-lvl 15', pam_radius_auth_conf)
@@ -225,59 +412,26 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
tmp = re.findall(r'group:\s+mapname\s+files', nsswitch_conf)
self.assertTrue(tmp)
- def test_system_login_radius_ipv6(self):
- # Verify generated RADIUS configuration files
+ # Login with proper credentials
+ out, err = self.ssh_send_cmd(ssh_test_command, username, password)
+ # verify login
+ self.assertFalse(err)
+ self.assertEqual(out, self.ssh_test_command_result)
- radius_key = 'VyOS-VyOS'
- radius_server = '2001:db8::1'
- radius_source = '::1'
- radius_port = '4000'
- radius_timeout = '4'
-
- self.cli_set(base_path + ['radius', 'server', radius_server, 'key', radius_key])
- self.cli_set(base_path + ['radius', 'server', radius_server, 'port', radius_port])
- self.cli_set(base_path + ['radius', 'server', radius_server, 'timeout', radius_timeout])
- self.cli_set(base_path + ['radius', 'source-address', radius_source])
- self.cli_set(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)])
-
- # check validate() - Only one IPv4 source-address supported
- with self.assertRaises(ConfigSessionError):
- self.cli_commit()
- self.cli_delete(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)])
+ # Login with invalid credentials
+ with self.assertRaises(paramiko.ssh_exception.AuthenticationException):
+ _, _ = self.ssh_send_cmd(ssh_test_command, username, f'{password}1')
+ # Remove RADIUS configuration
+ self.cli_delete(base_path + ['radius'])
+ # Remove RADIUS container
+ self.cli_delete(radius_container_path)
+ # Remove dummy interface
+ self.cli_delete(['interfaces', 'dummy', dummy_if])
self.cli_commit()
- # this file must be read with higher permissions
- pam_radius_auth_conf = cmd('sudo cat /etc/pam_radius_auth.conf')
- tmp = re.findall(r'\n?\[{}\]:{}\s+{}\s+{}\s+\[{}\]'.format(radius_server,
- radius_port, radius_key, radius_timeout,
- radius_source), pam_radius_auth_conf)
- self.assertTrue(tmp)
-
- # required, static options
- self.assertIn('priv-lvl 15', pam_radius_auth_conf)
- self.assertIn('mapped_priv_user radius_priv_user', pam_radius_auth_conf)
-
- # PAM
- pam_common_account = read_file('/etc/pam.d/common-account')
- self.assertIn('pam_radius_auth.so', pam_common_account)
-
- pam_common_auth = read_file('/etc/pam.d/common-auth')
- self.assertIn('pam_radius_auth.so', pam_common_auth)
-
- pam_common_session = read_file('/etc/pam.d/common-session')
- self.assertIn('pam_radius_auth.so', pam_common_session)
-
- pam_common_session_noninteractive = read_file('/etc/pam.d/common-session-noninteractive')
- self.assertIn('pam_radius_auth.so', pam_common_session_noninteractive)
-
- # NSS
- nsswitch_conf = read_file('/etc/nsswitch.conf')
- tmp = re.findall(r'passwd:\s+mapuid\s+files\s+mapname', nsswitch_conf)
- self.assertTrue(tmp)
-
- tmp = re.findall(r'group:\s+mapname\s+files', nsswitch_conf)
- self.assertTrue(tmp)
+ # Remove rendered tac_plus daemon configuration
+ shutil.rmtree(radius_config)
def test_system_login_max_login_session(self):
max_logins = '2'
@@ -300,11 +454,46 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
self.cli_delete(base_path + ['max-login-session'])
def test_system_login_tacacs(self):
- tacacs_secret = 'tac_plus_key'
+ tacacs_secret = ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(10))
tacacs_servers = ['100.64.0.11', '100.64.0.12']
+ source_address = '100.64.0.1'
+ dummy_if = 'dum12759'
+
+ # Load container image for lac_plus daemon
+ tac_plus_config = '/tmp/smoketest-tacacs-server'
+ tac_container_path = ['container', 'name', 'tacacs-1']
+
+ # Generate random string with 10 digits
+ username = 'tactest'
+ password = ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(10))
+ tac_test_user = {
+ 'username' : username,
+ 'password' : password,
+ 'tacacs_secret' : tacacs_secret,
+ 'source_address' : source_address,
+ }
+
+ tmpl = jinja2.Template(TAC_PLUS_TMPL_SRC)
+ write_file(f'{tac_plus_config}/tac_plus.cfg', tmpl.render(tac_test_user))
+
+ # Start tac_plus container
+ self.cli_set(tac_container_path + ['allow-host-networks'])
+ self.cli_set(tac_container_path + ['image', tac_image])
+ self.cli_set(tac_container_path + ['volume', 'config', 'destination', '/etc/tac_plus'])
+ self.cli_set(tac_container_path + ['volume', 'config', 'mode', 'ro'])
+ self.cli_set(tac_container_path + ['volume', 'config', 'source', tac_plus_config])
+
+ # Start container
+ self.cli_commit()
- # Enable TACACS
+ # Define TACACS traffic source address
+ self.cli_set(['interfaces', 'dummy', dummy_if, 'address', f'{source_address}/32'])
+ self.cli_set(base_path + ['tacacs', 'source-address', source_address])
+
+ # Define TACACS servers
for server in tacacs_servers:
+ # Use this system as "remote" TACACS server
+ self.cli_set(['interfaces', 'dummy', dummy_if, 'address', f'{server}/32'])
self.cli_set(base_path + ['tacacs', 'server', server, 'key', tacacs_secret])
self.cli_commit()
@@ -328,6 +517,11 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
self.assertIn('service=shell', pam_tacacs_conf)
self.assertIn('protocol=ssh', pam_tacacs_conf)
+ # Verify configured TACACS source address
+ self.assertIn(f'source_ip={source_address}', pam_tacacs_conf)
+ self.assertIn(f'source_ip={source_address}', nss_tacacs_conf)
+
+ # Verify configured TACACS servers
for server in tacacs_servers:
self.assertIn(f'secret={tacacs_secret}', pam_tacacs_conf)
self.assertIn(f'server={server}', pam_tacacs_conf)
@@ -335,6 +529,27 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'secret={tacacs_secret}', nss_tacacs_conf)
self.assertIn(f'server={server}', nss_tacacs_conf)
+ # Login with proper credentials
+ out, err = self.ssh_send_cmd(ssh_test_command, username, password)
+ # verify login
+ self.assertFalse(err)
+ self.assertEqual(out, self.ssh_test_command_result)
+
+ # Login with invalid credentials
+ with self.assertRaises(paramiko.ssh_exception.AuthenticationException):
+ _, _ = self.ssh_send_cmd(ssh_test_command, username, f'{password}1')
+
+ # Remove TACACS configuration
+ self.cli_delete(base_path + ['tacacs'])
+ # Remove tac_plus container
+ self.cli_delete(tac_container_path)
+ # Remove dummy interface
+ self.cli_delete(['interfaces', 'dummy', dummy_if])
+ self.cli_commit()
+
+ # Remove rendered tac_plus daemon configuration
+ shutil.rmtree(tac_plus_config)
+
def test_delete_current_user(self):
current_user = get_current_user()
@@ -344,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 ffb1d76ae..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,10 +16,15 @@
import os
import unittest
+
from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.cpu import get_cpus
from vyos.utils.file import read_file
from vyos.utils.process import is_systemd_service_active
from vyos.utils.system import sysctl_read
+from vyos.system import image
base_path = ['system', 'option']
@@ -27,6 +32,8 @@ 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'])
@@ -59,6 +66,7 @@ class TestSystemOption(VyOSUnitTestSHIM.TestCase):
def test_performance(self):
tuned_service = 'tuned.service'
+ path = ['system', 'sysctl', 'parameter']
self.assertFalse(is_systemd_service_active(tuned_service))
@@ -67,18 +75,18 @@ class TestSystemOption(VyOSUnitTestSHIM.TestCase):
gc_thresh2 = '262000'
gc_thresh3 = '524000'
- self.cli_set(['system', 'sysctl', 'parameter', 'net.ipv4.neigh.default.gc_thresh1', 'value', gc_thresh1])
- self.cli_set(['system', 'sysctl', 'parameter', 'net.ipv4.neigh.default.gc_thresh2', 'value', gc_thresh2])
- self.cli_set(['system', 'sysctl', 'parameter', 'net.ipv4.neigh.default.gc_thresh3', 'value', gc_thresh3])
+ self.cli_set(path + ['net.ipv4.neigh.default.gc_thresh1', 'value', gc_thresh1])
+ self.cli_set(path + ['net.ipv4.neigh.default.gc_thresh2', 'value', gc_thresh2])
+ self.cli_set(path + ['net.ipv4.neigh.default.gc_thresh3', 'value', gc_thresh3])
- self.cli_set(base_path + ['performance', 'throughput'])
+ self.cli_set(base_path + ['performance', 'network-throughput'])
self.cli_commit()
self.assertTrue(is_systemd_service_active(tuned_service))
- 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'
@@ -94,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, failfast=True)
+ 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 74c065473..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'
@@ -96,6 +96,39 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase):
for interface in Section.interfaces('ethernet'):
self.assertIn(f'pcap {{ dev={interface} }}', hsflowd)
+ def test_sflow_ipv6(self):
+ sampling_rate = '100'
+ default_polling = '30'
+ default_port = '6343'
+ sflow_server = {
+ '2001:db8::1': {},
+ '2001:db8::2': {'port': '8023'},
+ }
+
+ for interface in Section.interfaces('ethernet'):
+ self.cli_set(base_path + ['interface', interface])
+
+ self.cli_set(base_path + ['sampling-rate', sampling_rate])
+ for server, server_config in sflow_server.items():
+ self.cli_set(base_path + ['server', server])
+ if 'port' in server_config:
+ self.cli_set(base_path + ['server', server, 'port', server_config['port']])
+
+ # commit changes
+ self.cli_commit()
+
+ # verify configuration
+ hsflowd = read_file(hsflowd_conf)
+
+ self.assertIn(f'sampling={sampling_rate}', hsflowd)
+ self.assertIn(f'polling={default_polling}', hsflowd)
+
+ for server, server_config in sflow_server.items():
+ if 'port' in server_config:
+ self.assertIn(f'collector {{ ip = {server} udpport = {server_config["port"]} }}', hsflowd)
+ else:
+ self.assertIn(f'collector {{ ip = {server} udpport = {default_port} }}', hsflowd)
+
def test_vrf(self):
interface = 'eth0'
server = '192.0.2.1'
@@ -119,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 45a5b4087..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-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,23 +14,66 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import re
+import os
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.configsession import ConfigSessionError
from vyos.utils.file import read_file
+from vyos.utils.process import cmd
from vyos.utils.process import process_named_running
+from vyos.xml_ref import default_value
PROCESS_NAME = 'rsyslogd'
-RSYSLOG_CONF = '/etc/rsyslog.d/00-vyos.conf'
+RSYSLOG_CONF = '/run/rsyslog/rsyslog.conf'
+CERT_DIR = '/etc/rsyslog.d/certs'
base_path = ['system', 'syslog']
+base_logs_path = ['system', 'logs']
+pki_base = ['pki']
-def get_config_value(key):
- tmp = read_file(RSYSLOG_CONF)
- tmp = re.findall(r'\n?{}\s+(.*)'.format(key), tmp)
- return tmp[0]
+dummy_interface = 'dum372874'
+
+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
+ string: search for a specific start string in the configuration
+ """
+ command = 'cat /run/rsyslog/rsyslog.conf'
+ if string:
+ command += f' | sed -n "/^{string}$/,/}}/p"' # }} required to escape } in f-string
+ return cmd(command)
class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase):
@classmethod
@@ -40,40 +83,506 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase):
# ensure we can also run this test on a live system - so lets clean
# out the current configuration :)
cls.cli_delete(cls, base_path)
+ cls.cli_delete(cls, ['vrf'])
+ 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)
- def test_syslog_basic(self):
- host1 = '127.0.0.10'
- host2 = '127.0.0.20'
+ prifilt = ','.join(selectors)
+ self._assert_prifilt_sane(prifilt, facility_map)
+ return prifilt
- self.cli_set(base_path + ['host', host1, 'port', '999'])
- self.cli_set(base_path + ['host', host1, 'facility', 'all', 'level', 'all'])
- self.cli_set(base_path + ['host', host2, 'facility', 'kern', 'level', 'err'])
- self.cli_set(base_path + ['console', 'facility', 'all', 'level', 'warning'])
+ 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):
+ facility = {
+ 'all': {'level': 'warning'},
+ }
+ self._set_facilities(base_path + ['console'], facility)
self.cli_commit()
- # verify log level and facilities in config file
- # *.warning /dev/console
- # *.* @198.51.100.1:999
- # kern.err @192.0.2.1:514
- config = [get_config_value('\*.\*'), get_config_value('kern.err'), get_config_value('\*.warning')]
- expected = [f'@{host1}:999', f'@{host2}:514', '/dev/console']
-
- for i in range(0,3):
- self.assertIn(expected[i], config[i])
- # Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
+
+ 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)
+
+ 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'},
+ 'kern': {'level': 'debug'},
+ 'all': {'level': 'notice'},
+ }
+
+ self.cli_set(['system', 'host-name'], value=hostname)
+ self.cli_set(['system', 'domain-name'], value=domain_name)
+ self.cli_set(base_path + ['preserve-fqdn'])
+
+ self._set_facilities(base_path + ['local'], facility)
+
+ self.cli_commit()
+
+ config = get_config('')
+ expected = [
+ f'module(load="immark" interval="{default_marker_interval}")',
+ 'global(preserveFQDN="on")',
+ f'global(localHostname="{hostname}.{domain_name}")',
+ ]
+ for e in expected:
+ self.assertIn(e, config)
+
+ config = get_config('#### GLOBAL LOGGING ####')
+ 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)
+ 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'])
+ self.cli_commit()
+
+ config = get_config('')
+ self.assertNotIn('module(load="immark"', config)
+
+ def test_remote(self):
+ dummy_if_path = ['interfaces', 'dummy', dummy_interface]
+ rhosts = {
+ '169.254.0.1': {
+ 'facility': {'auth' : {'level': 'info'}},
+ 'protocol': 'udp',
+ },
+ '2001:db8::1': {
+ 'facility': {'all' : {'level': 'debug'}},
+ 'port': '1514',
+ 'protocol': 'udp',
+ },
+ 'syslog.vyos.net': {
+ 'facility': {'all' : {'level': 'debug'}},
+ 'port': '1515',
+ 'protocol': 'tcp',
+ },
+ '169.254.0.3': {
+ 'facility': {'auth' : {'level': 'info'},
+ 'kern' : {'level': 'debug'},
+ 'all' : {'level': 'notice'},
+ },
+ 'format': ['include-timezone', 'octet-counted'],
+ 'protocol': 'tcp',
+ 'port': '10514',
+ },
+ }
+ default_port = default_value(base_path + ['remote', next(iter(rhosts)), 'port'])
+ default_protocol = default_value(base_path + ['remote', next(iter(rhosts)), 'protocol'])
+
+ for remote, remote_options in rhosts.items():
+ remote_base = base_path + ['remote', remote]
+
+ if 'port' in remote_options:
+ self.cli_set(remote_base + ['port'], value=remote_options['port'])
+
+ if 'facility' in remote_options:
+ self._set_facilities(remote_base, remote_options['facility'])
+
+ if 'format' in remote_options:
+ for format in remote_options['format']:
+ self.cli_set(remote_base + ['format'], value=format)
+
+ if 'protocol' in remote_options:
+ protocol = remote_options['protocol']
+ self.cli_set(remote_base + ['protocol'], value=protocol)
+
+ if 'source_address' in remote_options:
+ source_address = remote_options['source_address']
+ self.cli_set(remote_base + ['source-address', source_address])
+
+ # check validate() - source address does not exist
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(dummy_if_path + ['address', f'{source_address}/32'])
+
+ self.cli_commit()
+
+ config = read_file(RSYSLOG_CONF)
+ for remote, remote_options in rhosts.items():
+ config = get_config(f'# Remote syslog to {remote}')
+ prifilt = ''
+ if 'facility' in remote_options:
+ prifilt = self._prifilt_selectors(remote_options['facility'])
+ if not prifilt:
+ # Skip test - as we do not render anything if no facility is set
+ continue
+
+ self.assertIn(f'if prifilt("{prifilt}") then {{', config)
+ self.assertIn( ' type="omfwd"', config)
+ self.assertIn(f' target="{remote}"', config)
+
+ port = default_port
+ if 'port' in remote_options:
+ port = remote_options['port']
+ self.assertIn(f'port="{port}"', config)
+
+ protocol = default_protocol
+ if 'protocol' in remote_options:
+ protocol = remote_options['protocol']
+ self.assertIn(f'protocol="{protocol}"', config)
+
+ if 'format' in remote_options:
+ if 'include-timezone' in remote_options['format']:
+ self.assertIn( ' template="RSYSLOG_SyslogProtocol23Format"', config)
+
+ if 'octet-counted' in remote_options['format']:
+ self.assertIn( ' TCP_Framing="octet-counted"', config)
+ else:
+ self.assertIn( ' TCP_Framing="traditional"', config)
+
+ # cleanup dummy interface
+ self.cli_delete(dummy_if_path)
+
+ def test_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': { },
+ '169.254.0.11': {
+ 'vrf': {'name' : 'red', 'table' : '12321'},
+ 'source_address' : '169.254.0.11',
+ },
+ '169.254.0.12': {
+ 'vrf': {'name' : 'green', 'table' : '12322'},
+ 'source_address' : '169.254.0.12',
+ },
+ '169.254.0.13': {
+ 'vrf': {'name' : 'blue', 'table' : '12323'},
+ 'source_address' : '169.254.0.13',
+ },
+ }
+
+ for remote, remote_options in rhosts.items():
+ remote_base = base_path + ['remote', remote]
+ self.cli_set(remote_base + ['facility', 'all'])
+
+ vrf = None
+ if 'vrf' in remote_options:
+ vrf = remote_options['vrf']['name']
+ self.cli_set(['vrf', 'name', vrf, 'table'],
+ value=remote_options['vrf']['table'])
+ self.cli_set(remote_base + ['vrf'], value=vrf)
+
+ if 'source_address' in remote_options:
+ source_address = remote_options['source_address']
+ self.cli_set(remote_base + ['source-address'],
+ value=source_address)
+
+ idx = source_address.split('.')[-1]
+ self.cli_set(['interfaces', 'dummy', f'dum{idx}', 'address'],
+ value=f'{source_address}/32')
+ if vrf:
+ self.cli_set(['interfaces', 'dummy', f'dum{idx}', 'vrf'],
+ value=vrf)
+
+ self.cli_commit()
+
+ for remote, remote_options in rhosts.items():
+ config = get_config(f'# Remote syslog to {remote}')
+
+ self.assertIn(f'target="{remote}"', config)
+ if 'vrf' in remote_options:
+ vrf = remote_options['vrf']['name']
+ self.assertIn(f'Device="{vrf}"', config)
+
+ if 'source_address' in remote_options:
+ source_address = remote_options['source_address']
+ self.assertIn(f'Address="{source_address}"', config)
+
+ # Cleanup VRF/Dummy interfaces
+ for remote, remote_options in rhosts.items():
+ if 'vrf' in remote_options:
+ vrf = remote_options['vrf']['name']
+ self.cli_delete(['vrf', 'name', vrf])
+
+ if 'source_address' in remote_options:
+ source_address = remote_options['source_address']
+ idx = source_address.split('.')[-1]
+ self.cli_delete(['interfaces', 'dummy', f'dum{idx}'])
if __name__ == '__main__':
- unittest.main(verbosity=2)
+ 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 3b8687b93..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-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,13 +16,17 @@
import os
import unittest
+import re
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
from vyos.ifconfig import Interface
+from vyos.utils.convert import encode_to_base64
from vyos.utils.process import process_named_running
from vyos.utils.file import read_file
+from vyos.xml_ref import default_value
+
ethernet_path = ['interfaces', 'ethernet']
tunnel_path = ['interfaces', 'tunnel']
@@ -43,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}'
@@ -106,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
@@ -125,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'])
@@ -146,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','')])
@@ -224,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
@@ -250,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:
@@ -273,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'
@@ -351,25 +582,262 @@ 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):
- tunnel_if = 'tun100'
- nhrp_secret = 'secret'
ike_lifetime = '3600'
esp_lifetime = '1800'
+ tunnel_if = "tun100"
+ tunnel_ip = '172.16.253.134/32'
+ tunnel_source = "192.0.2.134"
+ tunnel_encapsulation = "gre"
+ esp_group = "ESP-HUB"
+ ike_group = "IKE-HUB"
+ nhrp_secret = "vyos123"
+ nhrp_holdtime = '300'
+ nhs_tunnelip = '172.16.253.1'
+ nhs_nbmaip = '192.0.2.1'
+ map_tunnelip = '172.16.253.135'
+ map_nbmaip = "192.0.2.135"
+ nhrp_networkid = '1'
+
# Tunnel
- self.cli_set(tunnel_path + [tunnel_if, 'address', '172.16.253.134/29'])
- self.cli_set(tunnel_path + [tunnel_if, 'encapsulation', 'gre'])
- self.cli_set(tunnel_path + [tunnel_if, 'source-address', '192.0.2.1'])
- self.cli_set(tunnel_path + [tunnel_if, 'enable-multicast'])
- self.cli_set(tunnel_path + [tunnel_if, 'parameters', 'ip', 'key', '1'])
+ self.cli_set(tunnel_path + [tunnel_if, "address", tunnel_ip])
+ self.cli_set(tunnel_path + [tunnel_if, "encapsulation", tunnel_encapsulation])
+ self.cli_set(tunnel_path + [tunnel_if, "source-address", tunnel_source])
+ self.cli_set(tunnel_path + [tunnel_if, "enable-multicast"])
+ self.cli_set(tunnel_path + [tunnel_if, "parameters", "ip", "key", "1"])
# NHRP
- self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'cisco-authentication', nhrp_secret])
- self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'holding-time', '300'])
- self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'multicast', 'dynamic'])
- self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'redirect'])
- self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'shortcut'])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "authentication", nhrp_secret])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "holdtime", nhrp_holdtime])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "multicast", nhs_tunnelip])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "redirect"])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "shortcut"])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "registration-no-unique"])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "network-id", nhrp_networkid])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "nhs", "tunnel-ip", nhs_tunnelip, "nbma", nhs_nbmaip])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "map", "tunnel-ip", map_tunnelip, "nbma", map_nbmaip])
# IKE/ESP Groups
self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', esp_lifetime])
@@ -394,19 +862,23 @@ 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)
swanctl_lines = [
- f'proposals = aes128-sha1-modp1024,aes256-sha1-prfsha1-modp1024',
+ f'proposals = aes256-sha1-prfsha1-modp1024',
f'version = 1',
f'rekey_time = {ike_lifetime}s',
f'rekey_time = {esp_lifetime}s',
- f'esp_proposals = aes128-sha1-modp1024,aes256-sha1-modp1024,3des-md5-modp1024',
+ f'esp_proposals = aes256-sha1-modp1024,3des-md5-modp1024',
f'local_ts = dynamic[gre]',
f'remote_ts = dynamic[gre]',
f'mode = transport',
- f'secret = {nhrp_secret}'
+ f'secret = {nhrp_secret}',
+ 'unique = never',
]
for line in swanctl_lines:
self.assertIn(line, swanctl_conf)
@@ -489,12 +961,84 @@ 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'
local_id = 'vyos-r1'
remote_id = 'vyos-r2'
peer_base_path = base_path + ['site-to-site', 'peer', connection_name]
+ secret_base64 = encode_to_base64(secret)
self.cli_set(tunnel_path + ['tun1', 'encapsulation', 'gre'])
self.cli_set(tunnel_path + ['tun1', 'source-address', local_address])
@@ -509,7 +1053,8 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id])
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_address])
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip])
- self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret])
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret_base64])
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret-type', 'base64'])
self.cli_set(peer_base_path + ['authentication', 'local-id', local_id])
self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret'])
@@ -546,7 +1091,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
f'id-{regex_uuid4} = "{remote_id}"',
f'id-{regex_uuid4} = "{peer_ip}"',
f'id-{regex_uuid4} = "{local_address}"',
- f'secret = "{secret}"',
+ f'secret = 0s{secret_base64}',
]
for line in swanctl_secrets_lines:
@@ -896,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)
@@ -916,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):
@@ -947,7 +1505,8 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', ike_lifetime])
self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'dh-group', '14'])
self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'encryption', 'aes256'])
- self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha512'])
+ # a hash algorithm that cannot be mapped to an equivalent PRF
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'aes192gmac'])
# ESP
self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', eap_lifetime])
@@ -968,6 +1527,11 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'name-server', name_server])
self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'prefix', prefix])
+ # verify() - IKE group use not mapped hash algorithm
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha512'])
self.cli_commit()
self.assertTrue(os.path.exists(dhcp_interfaces_file))
@@ -980,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'
@@ -1355,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 2bb6c91c1..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-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,25 +18,30 @@ import re
import os
import unittest
-from base_vyostest_shim import VyOSUnitTestSHIM
from json import loads
from jmespath import search
+from base_vyostest_shim import VyOSUnitTestSHIM
+
from 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 = []
@@ -51,11 +56,15 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
else:
for tmp in Section.interfaces('ethernet', vlan=False):
cls._interfaces.append(tmp)
+
# 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')
@@ -65,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'
@@ -112,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}')
+ frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf')
self.assertIn(f' vni {table}', frrconfig)
self.assertEqual(int(table), get_vrf_tableid(vrf))
@@ -136,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:
@@ -156,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.
@@ -233,7 +280,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
self.assertTrue(interface_exists(vrf))
- frrconfig = self.getFRRconfig(f'vrf {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)
@@ -279,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:
@@ -297,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'
@@ -317,7 +406,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
# Verify route-map properly applied to FRR
for vrf in vrfs:
- frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon='zebra')
+ frrconfig = self.getFRRconfig(f'vrf {vrf}', 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)
@@ -332,8 +421,8 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
# Verify route-map properly is removed from FRR
for vrf in vrfs:
- frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon='zebra')
- self.assertNotIn(f'vrf {vrf}', frrconfig)
+ frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf')
+ self.assertNotIn(f' ip protocol', frrconfig)
def test_vrf_ip_ipv6_protocol_non_existing_route_map(self):
table = '6100'
@@ -380,7 +469,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
# Verify route-map properly applied to FRR
for vrf in vrfs:
- frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon='zebra')
+ frrconfig = self.getFRRconfig(f'vrf {vrf}', 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)
@@ -399,8 +488,8 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
# Verify route-map properly is removed from FRR
for vrf in vrfs:
- frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon='zebra')
- self.assertNotIn(f'vrf {vrf}', frrconfig)
+ frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf')
+ self.assertNotIn(f' ipv6 protocol', frrconfig)
def test_vrf_vni_duplicates(self):
base_table = '6300'
@@ -429,7 +518,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
for vrf in vrfs:
self.assertTrue(interface_exists(vrf))
- frrconfig = self.getFRRconfig(f'vrf {vrf}')
+ frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf')
self.assertIn(f' vni {table}', frrconfig)
# Increment table ID for the next run
table = str(int(table) + 1)
@@ -451,7 +540,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
for vrf in vrfs:
self.assertTrue(interface_exists(vrf))
- frrconfig = self.getFRRconfig(f'vrf {vrf}')
+ frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf')
self.assertIn(f' vni {table}', frrconfig)
# Increment table ID for the next run
table = str(int(table) + 1)
@@ -474,7 +563,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
for vrf in vrfs:
self.assertTrue(interface_exists(vrf))
- frrconfig = self.getFRRconfig(f'vrf {vrf}')
+ frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf')
self.assertIn(f' vni {table}', frrconfig)
# Increment table ID for the next run
table = str(int(table) + 2)
@@ -494,7 +583,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
for vrf in vrfs:
self.assertTrue(interface_exists(vrf))
- frrconfig = self.getFRRconfig(f'vrf {vrf}')
+ frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf')
self.assertIn(f' vni {table}', frrconfig)
# Increment table ID for the next run
table = str(int(table) + 2)
@@ -502,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}')
+ frrconfig = self.getFRRconfig(f'vrf {purple}', stop_section='^exit-vrf')
self.assertIn(f' vni {table}', frrconfig)
# Now delete all the VNIs
@@ -517,12 +606,12 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
for vrf in vrfs:
self.assertTrue(interface_exists(vrf))
- frrconfig = self.getFRRconfig(f'vrf {vrf}')
+ frrconfig = self.getFRRconfig(f'vrf {vrf}', stop_section='^exit-vrf')
self.assertNotIn('vni', frrconfig)
# Verify purple VNI remains
self.assertTrue(interface_exists(purple))
- frrconfig = self.getFRRconfig(f'vrf {purple}')
+ frrconfig = self.getFRRconfig(f'vrf {purple}', stop_section='^exit-vrf')
self.assertIn(f' vni {table}', frrconfig)
def test_vrf_ip_ipv6_nht(self):
@@ -540,7 +629,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
# Verify route-map properly applied to FRR
for vrf in vrfs:
- frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon='zebra')
+ frrconfig = self.getFRRconfig(f'vrf {vrf}', 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)
@@ -555,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}', daemon='zebra')
+ 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)
@@ -595,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())