diff options
| author | Christian Breunig <christian@breunig.cc> | 2026-06-16 17:24:43 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-06-16 17:24:43 +0200 |
| commit | c207f4a1ce9b13cd7e0664d35d805e633ebc69fb (patch) | |
| tree | 907fa52c7bb1779b7f9a574f9f9f4391e972e649 | |
| parent | 4fb32bc179127691f40c8e93c55877aa76f10416 (diff) | |
| parent | 43f4c41ded3fc9695e11c541069f89a137cd7a53 (diff) | |
| download | vyos-1x-c207f4a1ce9b13cd7e0664d35d805e633ebc69fb.tar.gz vyos-1x-c207f4a1ce9b13cd7e0664d35d805e633ebc69fb.zip | |
Merge pull request #5261 from inetman28/fix-vpp-lcp-vrf-table
VPP: T8972: Fix VPP/LCP VRF table synchronization for VPP-managed Ethernet interfaces and Ethernet VIFs.
| -rw-r--r-- | python/vyos/vpp/control_vpp.py | 106 | ||||
| -rw-r--r-- | python/vyos/vpp/utils.py | 2 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_vpp.py | 88 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces_ethernet.py | 29 |
4 files changed, 224 insertions, 1 deletions
diff --git a/python/vyos/vpp/control_vpp.py b/python/vyos/vpp/control_vpp.py index 1ad098b71..0746282de 100644 --- a/python/vyos/vpp/control_vpp.py +++ b/python/vyos/vpp/control_vpp.py @@ -26,6 +26,8 @@ from typing import TypeVar, ParamSpec, Literal from vpp_papi import VPPApiClient from vpp_papi import VPPIOError, VPPValueError +from vyos.vpp.utils import vpp_ip_addresses_by_index + # define types for static type checkers AnyType = TypeVar('AnyType') AnyParam = ParamSpec('AnyParam') @@ -301,6 +303,110 @@ class VPPControl: """ return self.__vpp_api_client.api.lcp_nl_resync() + @_Decorators.api_call + def get_interface_ip_table(self, ifname: str, is_ipv6: bool = False) -> int | None: + """Return the IP FIB table ID assigned to an interface. + + Args: + ifname (str): Interface name in VPP. + is_ipv6 (bool, optional): Return IPv6 table ID if True. Defaults to False. + + Returns: + int | None: FIB table ID or None if the interface is not found. + """ + iface_index = self.get_sw_if_index(ifname) + if iface_index is None: + return None + + response = self.__vpp_api_client.api.sw_interface_get_table( + sw_if_index=iface_index, + is_ipv6=is_ipv6, + ) + if response.retval != 0: + return None + + return response.vrf_id + + @_Decorators.api_call + def set_interface_ip_table( + self, ifname: str, table_id: int, is_ipv6: bool = False + ) -> None: + """Assign an interface to an IP FIB table. + + Args: + ifname (str): Interface name in VPP. + table_id (int): FIB table ID. + is_ipv6 (bool, optional): Set IPv6 table if True. Defaults to False. + """ + iface_index = self.get_sw_if_index(ifname) + if iface_index is None: + return None + + self.__vpp_api_client.api.sw_interface_set_table( + sw_if_index=iface_index, + is_ipv6=is_ipv6, + vrf_id=table_id, + ) + + @_Decorators.api_call + def add_del_interface_ip_address( + self, ifname: str, address: str, is_add: bool = True + ) -> None: + """Add or delete an interface IP address in VPP. + + Args: + ifname (str): Interface name in VPP. + address (str): Interface prefix, for example "192.0.2.1/24". + is_add (bool, optional): Add address if True, delete if False. + Defaults to True. + """ + iface_index = self.get_sw_if_index(ifname) + if iface_index is None: + return None + + self.__vpp_api_client.api.sw_interface_add_del_address( + sw_if_index=iface_index, + is_add=is_add, + del_all=False, + prefix=address, + ) + + def move_interface_to_ip_table_preserve_addresses( + self, ifname: str, table_id: int + ) -> None: + """Move an interface to an IP FIB table while preserving its addresses. + + VPP keeps connected routes in the interface IP table that was active when + addresses were added. Re-adding the same addresses after the table move + recreates connected routes in the target table. + + Args: + ifname (str): Interface name in VPP. + table_id (int): Target FIB table ID. + """ + + iface_index = self.get_sw_if_index(ifname) + if iface_index is None: + return None + + for is_ipv6 in [False, True]: + current_table = self.get_interface_ip_table(ifname, is_ipv6=is_ipv6) + if current_table is None or current_table == table_id: + continue + + addresses = vpp_ip_addresses_by_index( + self.__vpp_api_client.api, + iface_index, + is_ipv6=is_ipv6, + ) + for address in addresses: + self.add_del_interface_ip_address(ifname, address, is_add=False) + + self.set_interface_ip_table(ifname, table_id, is_ipv6=is_ipv6) + + for address in addresses: + self.add_del_interface_ip_address(ifname, address) + @_Decorators.check_retval @_Decorators.api_call def iface_rxmode(self, iface_name: str, rx_mode: str) -> None: diff --git a/python/vyos/vpp/utils.py b/python/vyos/vpp/utils.py index 10940b5a4..0badfdb19 100644 --- a/python/vyos/vpp/utils.py +++ b/python/vyos/vpp/utils.py @@ -194,7 +194,7 @@ def vpp_ip_addresses_by_index(vpp_api, index: int, is_ipv6: bool = False) -> lis ip_address_dump = vpp_api.ip_address_dump(sw_if_index=index, is_ipv6=is_ipv6) while ip_address_dump: ip_address_details = ip_address_dump.pop() - ip_addresses_list.append(str(ip_address_details._asdict().get('prefix'))) + ip_addresses_list.append(str(ip_address_details.prefix)) return ip_addresses_list diff --git a/smoketest/scripts/cli/test_vpp.py b/smoketest/scripts/cli/test_vpp.py index a506ad4f0..7c52dd10c 100755 --- a/smoketest/scripts/cli/test_vpp.py +++ b/smoketest/scripts/cli/test_vpp.py @@ -35,6 +35,7 @@ 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_ip_addresses_by_index from vyos.vpp.utils import vpp_iface_name_transform from vyos.vpp.config_resource_checks.resource_defaults import default_resource_map @@ -1513,6 +1514,93 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): list(acl_interfaces[0].acls)[: acl_interfaces[0].count], [acl_index] ) + def test_24_vpp_lcp_vrf_table_sync(self): + vlan = '20' + subif = f'{interface}.{vlan}' + address = '100.100.100.1/24' + fib_route = '100.100.100.1/32' + mgmt_vrf = 'mgmt' + test_vrf = 'test1' + + def assert_vpp_lcp_table(table_id): + vpp = VPPControl() + subif_index = vpp.get_sw_if_index(subif) + self.assertIsNotNone(subif_index) + self.assertEqual(vpp.get_interface_ip_table(subif), table_id) + self.assertIn(address, vpp_ip_addresses_by_index(vpp.api, subif_index)) + + def assert_fib_route(table_id, expected=True): + _, out = rc_cmd(f'sudo vppctl show ip fib table {table_id}') + if expected: + self.assertIn(fib_route, out) + else: + self.assertNotIn(fib_route, out) + + self.cli_set(['vrf', 'name', mgmt_vrf, 'table', '1000']) + self.cli_set( + ['interfaces', 'ethernet', interface, 'vif', vlan, 'address', address] + ) + self.cli_set( + ['interfaces', 'ethernet', interface, 'vif', vlan, 'vrf', mgmt_vrf] + ) + self.cli_commit() + + assert_vpp_lcp_table(1000) + assert_fib_route(0, expected=False) + assert_fib_route(1000) + + self.cli_delete(['interfaces', 'ethernet', interface, 'vif', vlan, 'address']) + self.cli_commit() + + vpp = VPPControl() + subif_index = vpp.get_sw_if_index(subif) + self.assertIsNotNone(subif_index) + self.assertEqual(vpp.get_interface_ip_table(subif), 1000) + self.assertNotIn(address, vpp_ip_addresses_by_index(vpp.api, subif_index)) + assert_fib_route(0, expected=False) + assert_fib_route(1000, expected=False) + + self.cli_set( + ['interfaces', 'ethernet', interface, 'vif', vlan, 'address', address] + ) + self.cli_commit() + + assert_vpp_lcp_table(1000) + assert_fib_route(0, expected=False) + assert_fib_route(1000) + + self.cli_delete(['interfaces', 'ethernet', interface, 'vif', vlan, 'vrf']) + self.cli_commit() + + assert_vpp_lcp_table(0) + assert_fib_route(0) + assert_fib_route(1000, expected=False) + + self.cli_set( + ['interfaces', 'ethernet', interface, 'vif', vlan, 'vrf', mgmt_vrf] + ) + self.cli_commit() + + assert_vpp_lcp_table(1000) + assert_fib_route(0, expected=False) + assert_fib_route(1000) + + self.cli_set(['vrf', 'name', test_vrf, 'table', '2000']) + self.cli_set( + ['interfaces', 'ethernet', interface, 'vif', vlan, 'vrf', test_vrf] + ) + self.cli_commit() + + assert_vpp_lcp_table(2000) + assert_fib_route(0, expected=False) + assert_fib_route(1000, expected=False) + assert_fib_route(2000) + + self.cli_delete(['interfaces', 'ethernet', interface, 'vif', vlan]) + self.cli_delete(['vrf', 'name', test_vrf]) + self.cli_delete(['vrf', 'name', mgmt_vrf]) + self.cli_commit() + if __name__ == '__main__': unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/src/conf_mode/interfaces_ethernet.py b/src/conf_mode/interfaces_ethernet.py index 10b778eea..3b8453780 100755 --- a/src/conf_mode/interfaces_ethernet.py +++ b/src/conf_mode/interfaces_ethernet.py @@ -45,6 +45,7 @@ from vyos.utils.dict import dict_search from vyos.utils.dict import dict_to_paths_values from vyos.utils.dict import dict_set from vyos.utils.dict import dict_delete +from vyos.utils.network import get_vrf_tableid from vyos.utils.process import is_systemd_service_running from vyos.vpp.config_verify import verify_vpp_remove_interface from vyos.vpp.control_vpp import VPPControl @@ -473,8 +474,36 @@ def apply(ethernet): mtu = e.get_mtu() vpp_api.set_iface_mtu(lcp_name, mtu) + sync_vpp_lcp_vrf_tables(ethernet, vpp_api) + return None + +def sync_vpp_lcp_vrf_tables(ethernet: dict, vpp_api: VPPControl) -> None: + """Synchronize VyOS VRF assignment to VPP IP FIB table binding.""" + + def iter_vpp_lcp_vrf_targets() -> list[tuple[str, int]]: + interfaces = [ethernet['ifname']] + for vif in ethernet.get('vif', {}).values(): + interfaces.append(vif['ifname']) + for vif_s in ethernet.get('vif_s', {}).values(): + interfaces.append(vif_s['ifname']) + for vif_c in vif_s.get('vif_c', {}).values(): + interfaces.append(vif_c['ifname']) + return [(iface, get_vrf_tableid(iface) or 0) for iface in interfaces] + + lcp_vpp_ifaces = { + pair.get('vpp_name_hw') + for pair in vpp_api.lcp_pairs_list() + if pair.get('vpp_name_hw') + } + + for ifname, table_id in iter_vpp_lcp_vrf_targets(): + if ifname not in lcp_vpp_ifaces: + continue + + vpp_api.move_interface_to_ip_table_preserve_addresses(ifname, table_id) + if __name__ == '__main__': try: c = get_config() |
