summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorViacheslav Hletenko <v.gletenko@vyos.io>2026-07-07 16:33:17 +0300
committerGitHub <noreply@github.com>2026-07-07 16:33:17 +0300
commitb02982d2a17e3a8a344f03eef9046c9e85a8f15e (patch)
treedcbd2763c38272c86482171fde6870e63ac33b2e
parent6f2b5a12e29deb46c9a724f8405d9b4f67e6420f (diff)
parent021f9c1b2be91e246e560a67396e54133e9eb626 (diff)
downloadvyos-1x-b02982d2a17e3a8a344f03eef9046c9e85a8f15e.tar.gz
vyos-1x-b02982d2a17e3a8a344f03eef9046c9e85a8f15e.zip
Merge pull request #5313 from c-po/pppoe-default-route
frrender: T9054: keep PPPoE/DHCP default route when "protocols static" is deleted
-rw-r--r--python/vyos/frrender.py22
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_pppoe.py38
2 files changed, 58 insertions, 2 deletions
diff --git a/python/vyos/frrender.py b/python/vyos/frrender.py
index 7c9a72c50..a905c9a00 100644
--- a/python/vyos/frrender.py
+++ b/python/vyos/frrender.py
@@ -467,7 +467,9 @@ def get_frrender_dict(conf: Config, argv=None) -> dict:
no_tag_node_value_mangle=True)
dict.update({'static' : static})
elif conf.exists_effective(static_cli_path):
- dict.update({'static' : deleted_protocol})
+ # Use a copy - static.dhcp/static.pppoe below may be merged in,
+ # and deleted_protocol is shared by reference with other protocols
+ dict.update({'static' : deleted_protocol.copy()})
# We need to check the CLI if the NHRP node is present and thus load in all the default
# values present on the CLI - that's why we have if conf.exists()
@@ -488,6 +490,12 @@ def get_frrender_dict(conf: Config, argv=None) -> dict:
tmp = get_pppoe_interfaces(conf)
if tmp: dict_set_nested('static.pppoe', tmp, dict)
+ # T6991/T9054: "protocols static" may have been deleted while a DHCP or
+ # PPPoE interface still contributes a default route - in that case the
+ # static section must still be rendered, so drop the deletion marker
+ if 'static' in dict and ('dhcp' in dict['static'] or 'pppoe' in dict['static']):
+ dict['static'].pop('deleted', None)
+
# keep a re-usable list of dependent VRFs
dependent_vrfs_default = {}
if 'bgp' in dict:
@@ -615,7 +623,9 @@ def get_frrender_dict(conf: Config, argv=None) -> dict:
no_tag_node_value_mangle=True)
dict_set_nested(f'{protocol_dict_path}.static', static, vrf)
elif conf.exists_effective(static_vrf_path):
- dict_set_nested(f'{protocol_dict_path}.static', deleted_protocol, vrf)
+ # Use a copy - static.dhcp/static.pppoe below may be merged in,
+ # and deleted_protocol is shared by reference with other protocols
+ dict_set_nested(f'{protocol_dict_path}.static', deleted_protocol.copy(), vrf)
# T3680 - get a list of all interfaces currently configured to use DHCP
tmp = get_dhcp_interfaces(conf, vrf_name)
@@ -624,6 +634,14 @@ def get_frrender_dict(conf: Config, argv=None) -> dict:
tmp = get_pppoe_interfaces(conf, vrf_name)
if tmp: dict_set_nested(f'name.{vrf_name}.protocols.static.pppoe', tmp, vrf)
+ # T6991/T9054: "protocols static" may have been deleted while a
+ # DHCP or PPPoE interface still contributes a default route - in
+ # that case the static section must still be rendered, so drop
+ # the deletion marker
+ static_dict = dict_search(f'{protocol_dict_path}.static', vrf)
+ if static_dict and ('dhcp' in static_dict or 'pppoe' in static_dict):
+ static_dict.pop('deleted', None)
+
vrf_vni_path = ['vrf', 'name', vrf_name, 'vni']
if conf.exists(vrf_vni_path):
vrf_config.update({'vni': conf.return_value(vrf_vni_path)})
diff --git a/smoketest/scripts/cli/test_interfaces_pppoe.py b/smoketest/scripts/cli/test_interfaces_pppoe.py
index b8b0429a8..2c240d20e 100755
--- a/smoketest/scripts/cli/test_interfaces_pppoe.py
+++ b/smoketest/scripts/cli/test_interfaces_pppoe.py
@@ -416,5 +416,43 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase):
# Validate and verify assigned IP addresses
self._verify_interface_address(interface)
+ def test_pppoe_default_route_survives_static_deletion(self):
+ # T6991/T9054: The PPPoE default route must not be withdrawn from FRR
+ # when "protocols static" is deleted - only the statically configured
+ # routes must disappear, the PPPoE-sourced default route must stay.
+ interface = self._interfaces[0]
+ (user, passwd) = self.u_p_dict[interface]
+ static_base_path = ['protocols', 'static']
+
+ 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_commit()
+
+ self.assertTrue(wait_for_interface(interface),
+ msg=f'Interface {interface} not found after {connect_timeout} seconds!')
+
+ default_route = rf'ip route 0.0.0.0/0 {interface} tag 210'
+ frrconfig = self.getFRRconfig('')
+ self.assertIn(default_route, frrconfig)
+
+ # Add an unrelated static route - this is what triggers "protocols
+ # static" to exist on the CLI in the first place
+ self.cli_set(static_base_path + ['route', '10.0.0.0/8', 'blackhole'])
+ self.cli_commit()
+
+ frrconfig = self.getFRRconfig('')
+ self.assertIn(default_route, frrconfig)
+ self.assertIn(r'ip route 10.0.0.0/8 blackhole', frrconfig)
+
+ # Now delete "protocols static" entirely - the PPPoE default route
+ # must remain in the FRR configuration (and thus in the RIB/FIB)
+ self.cli_delete(static_base_path)
+ self.cli_commit()
+
+ frrconfig = self.getFRRconfig('')
+ self.assertNotIn(r'ip route 10.0.0.0/8 blackhole', frrconfig)
+ self.assertIn(default_route, frrconfig)
+
if __name__ == '__main__':
unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on())