diff options
| author | Viacheslav Hletenko <v.gletenko@vyos.io> | 2025-10-22 15:39:18 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-22 15:39:18 +0300 |
| commit | c284938df61b5e970dd13cde03e0693254e89211 (patch) | |
| tree | 8339e1bbe531ac5caffb80938b48fdce4743cbba | |
| parent | d272b2509836403682c6c8ab4096ccf4be003bcb (diff) | |
| parent | c4632bb6ad6c055fb7fe5a5c055354c6895ba8e5 (diff) | |
| download | vyos-1x-c284938df61b5e970dd13cde03e0693254e89211.tar.gz vyos-1x-c284938df61b5e970dd13cde03e0693254e89211.zip | |
Merge pull request #4622 from MattKobayashi/T3680
T3680: protocols: add dhclient hooks for dhcp-interface static routes
| -rw-r--r-- | python/vyos/defaults.py | 2 | ||||
| -rw-r--r-- | python/vyos/frrender.py | 15 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_protocols_static.py | 107 | ||||
| -rwxr-xr-x | src/conf_mode/protocols_static.py | 20 | ||||
| -rwxr-xr-x | src/etc/dhcp/dhclient-enter-hooks.d/98-vyos-static-routes-dhclient-hook | 33 | ||||
| -rwxr-xr-x | src/etc/dhcp/dhclient-exit-hooks.d/97-run-user-hooks (renamed from src/etc/dhcp/dhclient-exit-hooks.d/98-run-user-hooks) | 0 | ||||
| -rwxr-xr-x | src/etc/dhcp/dhclient-exit-hooks.d/98-vyos-static-routes-dhclient-hook | 39 | ||||
| -rwxr-xr-x | src/helpers/vyos-request-configd-update.py | 31 | ||||
| -rwxr-xr-x | src/services/vyos-configd | 3 |
9 files changed, 248 insertions, 2 deletions
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index 43cde38e9..25d7fd50c 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -61,6 +61,8 @@ config_files = { config_status = '/tmp/vyos-config-status' api_config_state = '/run/http-api-state' frr_debug_enable = '/tmp/vyos.frr.debug' +static_route_dhcp_interfaces_path = '/tmp/static_dhcp_interfaces' +vyos_configd_socket_path = 'ipc:///run/vyos-configd.sock' cfg_group = 'vyattacfg' diff --git a/python/vyos/frrender.py b/python/vyos/frrender.py index decec8aa2..91fd3667d 100644 --- a/python/vyos/frrender.py +++ b/python/vyos/frrender.py @@ -31,11 +31,14 @@ from vyos.config import config_dict_merge from vyos.configdict import get_dhcp_interfaces from vyos.configdict import get_pppoe_interfaces from vyos.defaults import frr_debug_enable +from vyos.defaults import static_route_dhcp_interfaces_path from vyos.utils.dict import dict_search from vyos.utils.dict import dict_set_nested +from vyos.utils.file import read_file from vyos.utils.file import write_file from vyos.utils.process import cmd from vyos.utils.process import rc_cmd +from vyos.template import get_dhcp_router from vyos.template import render_to_string from vyos import ConfigError @@ -634,6 +637,7 @@ def get_frrender_dict(conf: Config, argv=None) -> dict: class FRRender: cached_config_dict = {} + cached_dhcp_gateways = {} def __init__(self): self._frr_conf = '/run/frr/config/vyos.frr.conf' @@ -646,11 +650,20 @@ class FRRender: tmp = type(config_dict) raise ValueError(f'Config must be of type "dict" and not "{tmp}"!') + dhcp_gateways = { + interface: get_dhcp_router(interface) + for interface in read_file(static_route_dhcp_interfaces_path, '').split() + } - if self.cached_config_dict == config_dict: + if ( + self.cached_config_dict == config_dict + and self.cached_dhcp_gateways == dhcp_gateways + ): debug('FRR: NO CHANGES DETECTED') return False + self.cached_config_dict = config_dict + self.cached_dhcp_gateways = dhcp_gateways def inline_helper(config_dict) -> str: output = '!\n' diff --git a/smoketest/scripts/cli/test_protocols_static.py b/smoketest/scripts/cli/test_protocols_static.py index 03115b7b8..77a45e333 100755 --- a/smoketest/scripts/cli/test_protocols_static.py +++ b/smoketest/scripts/cli/test_protocols_static.py @@ -651,5 +651,112 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): 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') + + 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') + 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=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py index 7c730ca81..6310be13d 100755 --- a/src/conf_mode/protocols_static.py +++ b/src/conf_mode/protocols_static.py @@ -17,6 +17,7 @@ from ipaddress import IPv4Network from sys import exit from sys import argv +import os from vyos.config import Config from vyos.configverify import has_frr_protocol_in_dict @@ -25,12 +26,15 @@ from vyos.configverify import verify_vrf from vyos.frrender import FRRender from vyos.frrender import get_frrender_dict from vyos.utils.process import is_systemd_service_running +from vyos.utils.file import write_file from vyos.template import render from vyos import ConfigError from vyos import airbag +from vyos import defaults airbag.enable() config_file = '/etc/iproute2/rt_tables.d/vyos-static.conf' +DHCP_HOOK_IFLIST = defaults.static_route_dhcp_interfaces_path def get_config(config=None): if config: @@ -92,6 +96,22 @@ def generate(config_dict): # eqivalent of the C foo ? 'a' : 'b' statement static = vrf and config_dict['vrf']['name'][vrf]['protocols']['static'] or config_dict['static'] + # Collect interfaces that have DHCP configuration for DHCP hooks + dhcp_interfaces = set() + + # Check for DHCP interfaces in route configurations + if 'route' in static: + for prefix, prefix_options in static['route'].items(): + if 'dhcp_interface' in prefix_options: + for interface_name in prefix_options['dhcp_interface']: + dhcp_interfaces.add(interface_name) + + # Write the interface list for DHCP hooks or clean up if empty + if dhcp_interfaces: + write_file(DHCP_HOOK_IFLIST, " ".join(dhcp_interfaces)) + elif os.path.exists(DHCP_HOOK_IFLIST): + os.unlink(DHCP_HOOK_IFLIST) + # Put routing table names in /etc/iproute2/rt_tables render(config_file, 'iproute2/static.conf.j2', static) diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/98-vyos-static-routes-dhclient-hook b/src/etc/dhcp/dhclient-enter-hooks.d/98-vyos-static-routes-dhclient-hook new file mode 100755 index 000000000..439a8dd87 --- /dev/null +++ b/src/etc/dhcp/dhclient-enter-hooks.d/98-vyos-static-routes-dhclient-hook @@ -0,0 +1,33 @@ +#!/bin/bash +# +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> +# +# This program is 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/>. + +DHCP_HOOK_IFLIST="/tmp/static_dhcp_interfaces" + +# Only run if there are static routes with dhcp-interface configured +if ! { [ -f $DHCP_HOOK_IFLIST ] && grep -qw $interface $DHCP_HOOK_IFLIST; }; then + return 0 +fi + +# Handle interface state changes that require static route regeneration +# - PREINIT: interface is about to be configured, cleanup old routes +# - EXPIRE: lease has expired, remove routes +# - FAIL: DHCP failed, remove routes +# - RELEASE: lease released, remove routes +# - STOP: dhclient stopped, remove routes +if [ "$reason" == "PREINIT" ] || [ "$reason" == "EXPIRE" ] || [ "$reason" == "FAIL" ] || [ "$reason" == "RELEASE" ] || [ "$reason" == "STOP" ]; then + # Re-generate static routes config to remove routes that depend on this interface + sudo /usr/libexec/vyos/vyos-request-configd-update.py +fi diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/98-run-user-hooks b/src/etc/dhcp/dhclient-exit-hooks.d/97-run-user-hooks index 910b586f0..910b586f0 100755 --- a/src/etc/dhcp/dhclient-exit-hooks.d/98-run-user-hooks +++ b/src/etc/dhcp/dhclient-exit-hooks.d/97-run-user-hooks diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/98-vyos-static-routes-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/98-vyos-static-routes-dhclient-hook new file mode 100755 index 000000000..7038bf25e --- /dev/null +++ b/src/etc/dhcp/dhclient-exit-hooks.d/98-vyos-static-routes-dhclient-hook @@ -0,0 +1,39 @@ +#!/bin/bash +# +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> +# +# This program is 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/>. + +DHCP_HOOK_IFLIST="/tmp/static_dhcp_interfaces" + +# Only run if there are static routes with dhcp-interface configured +if ! { [ -f $DHCP_HOOK_IFLIST ] && grep -qw $interface $DHCP_HOOK_IFLIST; }; then + return 0 +fi + +# Re-generate the config on the following events: +# - BOUND: always re-generate +# - RENEW: re-generate if the IP address changed +# - REBIND: re-generate if the IP address changed +# - EXPIRE: always re-generate (route should be removed) +# - RELEASE: always re-generate (route should be removed) +if [ "$reason" == "RENEW" ] || [ "$reason" == "REBIND" ]; then + if [ "$old_routers" == "$new_routers" ]; then + return 0 + fi +elif [ "$reason" != "BOUND" ] && [ "$reason" != "EXPIRE" ] && [ "$reason" != "RELEASE" ]; then + return 0 +fi + +# Re-generate the static routes config +sudo /usr/libexec/vyos/vyos-request-configd-update.py diff --git a/src/helpers/vyos-request-configd-update.py b/src/helpers/vyos-request-configd-update.py new file mode 100755 index 000000000..55ed0eef9 --- /dev/null +++ b/src/helpers/vyos-request-configd-update.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 + +import json +import zmq + +from vyos.utils.commit import wait_for_commit_lock +from vyos.defaults import vyos_configd_socket_path + +context = zmq.Context() + +request = { + 'type': 'node', + 'last': True, + 'data': '/usr/libexec/vyos/conf_mode/protocols_static.py', +} +request = json.dumps(request) + +print("Waiting for commit lock...") +wait_for_commit_lock() + +print("Connecting to vyos-configd server...") +socket = context.socket(zmq.REQ) +socket.connect(vyos_configd_socket_path) + +print(f"Sending request {request}...") +socket.send_string(request) + +message = socket.recv() +print(f"Received reply {request} [ {message} ]") + +print("All done") diff --git a/src/services/vyos-configd b/src/services/vyos-configd index 1a207f876..2f060dc82 100755 --- a/src/services/vyos-configd +++ b/src/services/vyos-configd @@ -33,6 +33,7 @@ from enum import Enum import zmq from vyos.defaults import directories +from vyos.defaults import vyos_configd_socket_path from vyos.utils.boot import boot_configuration_complete from vyos.configsource import ConfigSourceString from vyos.configsource import ConfigSourceError @@ -57,7 +58,7 @@ if debug: else: logger.setLevel(logging.INFO) -SOCKET_PATH = 'ipc:///run/vyos-configd.sock' +SOCKET_PATH = vyos_configd_socket_path MAX_MSG_SIZE = 65535 PAD_MSG_SIZE = 6 |
