diff options
| author | Viacheslav Hletenko <v.gletenko@vyos.io> | 2026-03-04 15:33:41 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-04 15:33:41 +0200 |
| commit | 9fae68c92fe769da4f877c8df358868812a660ad (patch) | |
| tree | 95d8b0c47cae0efcca44509a8f01ab6244f09900 | |
| parent | 2a4d52b2b8c39f4f77b526ce80686247dd4d0f95 (diff) | |
| parent | 32a5763872420ba8af53e3371c5c68e7369b5e62 (diff) | |
| download | vyos-1x-9fae68c92fe769da4f877c8df358868812a660ad.tar.gz vyos-1x-9fae68c92fe769da4f877c8df358868812a660ad.zip | |
Merge pull request #5024 from natali-rs1985/T8328
vpp: T8328: Migrate xconnect interface to 'interfaces vpp xconnect'
| -rw-r--r-- | interface-definitions/vpp.xml.in | 67 | ||||
| -rw-r--r-- | interface-definitions/vpp_interface_xconnnect.xml.in | 49 | ||||
| -rw-r--r-- | python/vyos/ifconfig/vpp/__init__.py | 2 | ||||
| -rw-r--r-- | python/vyos/ifconfig/vpp/xconnect.py | 98 | ||||
| -rw-r--r-- | python/vyos/vpp/config_deps.py | 4 | ||||
| -rw-r--r-- | python/vyos/vpp/config_verify.py | 4 | ||||
| -rw-r--r-- | python/vyos/vpp/interface/__init__.py | 2 | ||||
| -rw-r--r-- | python/vyos/vpp/interface/xconnect.py | 95 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_vpp.py | 25 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_xconnect.py | 73 | ||||
| -rw-r--r-- | src/migration-scripts/vpp/5-to-6 | 21 |
11 files changed, 207 insertions, 233 deletions
diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in index 141c40275..e0389542b 100644 --- a/interface-definitions/vpp.xml.in +++ b/interface-definitions/vpp.xml.in @@ -6,73 +6,6 @@ <priority>295</priority> </properties> <children> - <node name='interfaces'> - <properties> - <help>Network interfaces</help> - <priority>300</priority> - </properties> - <children> - <!-- Disable as geneve interface index in the VPP always 0 and not configured --> - <!-- <tagNode name="geneve" owner="${vyos_conf_scripts_dir}/vpp_interfaces_geneve.py"> - <properties> - <priority>325</priority> - <help>Generic Network Virtualization Encapsulation (GENEVE) Interface</help> - <constraint> - <regex>geneve[0-9]+</regex> - </constraint> - <constraintErrorMessage>GENEVE interface must be named geneveN</constraintErrorMessage> - <valueHelp> - <format>geneveN</format> - <description>GENEVE interface name</description> - </valueHelp> - </properties> - <children> --> - <!-- #include <include/source-address-ipv4-ipv6.xml.i> --> - <!-- #include <include/interface/tunnel-remote.xml.i> --> - <!-- #include <include/vni.xml.i> --> - <!-- #include <include/kernel-interface-tun.xml.i> --> - <!-- </children> --> - <!-- </tagNode> --> - <tagNode name="xconnect" owner="${vyos_conf_scripts_dir}/vpp_interfaces_xconnect.py"> - <properties> - <help>Layer 2 cross connect</help> - <priority>325</priority> - <constraint> - <regex>xcon[0-9]+</regex> - </constraint> - <constraintErrorMessage>Cross connect must be named xconN</constraintErrorMessage> - <valueHelp> - <format>xconN</format> - <description>Cross connect interface name</description> - </valueHelp> - </properties> - <children> - #include <include/generic-description.xml.i> - #include <include/interface/disable.xml.i> - <node name="member"> - <properties> - <help>Cross connect pair mamber interfaces</help> - </properties> - <children> - <leafNode name="interface"> - <properties> - <help>Member interface name</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces --bridgeable</script> - </completionHelp> - <valueHelp> - <format>txt</format> - <description>Interface name</description> - </valueHelp> - <multi/> - </properties> - </leafNode> - </children> - </node> - </children> - </tagNode> - </children> - </node> <node name="ipfix" owner="${vyos_conf_scripts_dir}/vpp_ipfix.py"> <properties> <help>IP Flow Information Export (IPFIX) configuration</help> diff --git a/interface-definitions/vpp_interface_xconnnect.xml.in b/interface-definitions/vpp_interface_xconnnect.xml.in new file mode 100644 index 000000000..3480a0d41 --- /dev/null +++ b/interface-definitions/vpp_interface_xconnnect.xml.in @@ -0,0 +1,49 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name='interfaces'> + <children> + <node name='vpp'> + <children> + <tagNode name="xconnect" owner="${vyos_conf_scripts_dir}/vpp_interfaces_xconnect.py"> + <properties> + <help>Layer 2 cross-connect</help> + <priority>325</priority> + <constraint> + <regex>vppxcon[0-9]+</regex> + </constraint> + <constraintErrorMessage>Cross-connect interface must be named vppxconN</constraintErrorMessage> + <valueHelp> + <format>vppxconN</format> + <description>Cross-connect interface name</description> + </valueHelp> + </properties> + <children> + #include <include/generic-description.xml.i> + #include <include/interface/disable.xml.i> + <node name="member"> + <properties> + <help>Cross-connect pair member interfaces</help> + </properties> + <children> + <leafNode name="interface"> + <properties> + <help>Member interface name</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces --bridgeable</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + <multi/> + </properties> + </leafNode> + </children> + </node> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/ifconfig/vpp/__init__.py b/python/vyos/ifconfig/vpp/__init__.py index 38f72e42f..a0a1f6930 100644 --- a/python/vyos/ifconfig/vpp/__init__.py +++ b/python/vyos/ifconfig/vpp/__init__.py @@ -22,6 +22,7 @@ from .interface import VPPInterface from .ipip import VPPIPIPInterface from .loopback import VPPLoopbackInterface from .vxlan import VPPVXLANInterface +from .xconnect import VPPXconnectInterface __all__ = [ 'VPPBondInterface', @@ -31,4 +32,5 @@ __all__ = [ 'VPPIPIPInterface', 'VPPLoopbackInterface', 'VPPVXLANInterface', + 'VPPXconnectInterface', ] diff --git a/python/vyos/ifconfig/vpp/xconnect.py b/python/vyos/ifconfig/vpp/xconnect.py new file mode 100644 index 000000000..a5d7d15fd --- /dev/null +++ b/python/vyos/ifconfig/vpp/xconnect.py @@ -0,0 +1,98 @@ +# +# 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. + +from vyos.ifconfig.vpp.interface import VPPInterface +from vyos.vpp.utils import iftunnel_transform + + +def _transform_members(members): + """ + Transform interface names to VPP names for xconnect operations. + + Args: + members (list of str): List of two interface names. + + Returns: + tuple: Transformed first and second interface names. + """ + interface_transform_filter = ('vxlan',) + first_member = members[0].removeprefix('vpp') + second_member = members[1].removeprefix('vpp') + + # Check if member in required filter to transform 'vxlanX' => 'vxlan_tunnelX' + if first_member.startswith(interface_transform_filter): + first_member = iftunnel_transform(first_member) + if second_member.startswith(interface_transform_filter): + second_member = iftunnel_transform(second_member) + + return first_member, second_member + + +class VPPXconnectInterface(VPPInterface): + def __init__(self, ifname): + self.vpp_ifname = ifname + + super().__init__(self.vpp_ifname) + + def add_l2_xconnect(self, first, second): + """Add l2 cross connect + Example: + from vyos.ifconfig.vpp import VPPXconnectInterface + a = VPPXconnectInterface(ifname='vppxcon0') + a.add_l2_xconnect(first, second) + """ + member_first_if_index = self.vpp.get_sw_if_index(first) + member_second_if_index = self.vpp.get_sw_if_index(second) + self.vpp.api.sw_interface_set_l2_xconnect( + rx_sw_if_index=member_first_if_index, + tx_sw_if_index=member_second_if_index, + enable=True, + ) + self.vpp.api.sw_interface_set_l2_xconnect( + rx_sw_if_index=member_second_if_index, + tx_sw_if_index=member_first_if_index, + enable=True, + ) + + def del_l2_xconnect(self, first, second): + """Move l2 cross connect member to mode l3 (delete xconnect) + Example: + from vyos.ifconfig.vpp import VPPXconnectInterface + a = VPPXconnectInterface(ifname='vppxcon0') + a.del_l2_xconnect(first, second) + """ + member_first_if_index = self.vpp.get_sw_if_index(first) + member_second_if_index = self.vpp.get_sw_if_index(second) + self.vpp.api.sw_interface_set_l2_xconnect( + rx_sw_if_index=member_first_if_index, + tx_sw_if_index=member_second_if_index, + enable=False, + ) + self.vpp.api.sw_interface_set_l2_xconnect( + rx_sw_if_index=member_second_if_index, + tx_sw_if_index=member_first_if_index, + enable=False, + ) + + def remove(self, members): + first, second = _transform_members(members) + self.del_l2_xconnect(first, second) + + def update(self, config): + members = config['member']['interface'] + first, second = _transform_members(members) + self.add_l2_xconnect(first, second) diff --git a/python/vyos/vpp/config_deps.py b/python/vyos/vpp/config_deps.py index 4aab90a4a..43cf6036f 100644 --- a/python/vyos/vpp/config_deps.py +++ b/python/vyos/vpp/config_deps.py @@ -31,7 +31,7 @@ def deps_xconnect_dict(conf) -> dict[str, list[str]]: """ xconn_members_dict: dict[str, list[str]] = {} config = conf.get_config_dict( - ['vpp', 'interfaces', 'xconnect'], + ['interfaces', 'vpp', 'xconnect'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True, @@ -39,7 +39,7 @@ def deps_xconnect_dict(conf) -> dict[str, list[str]]: for xconn_name, xconn_config in config.items(): for member_name in xconn_config.get('member', {}).get('interface', []): - xconn_ifaces_list = xconn_members_dict.get(xconn_name, []) + xconn_ifaces_list = xconn_members_dict.get(member_name, []) xconn_ifaces_list.append(xconn_name) xconn_members_dict.update({member_name: xconn_ifaces_list}) diff --git a/python/vyos/vpp/config_verify.py b/python/vyos/vpp/config_verify.py index dab22cf8f..d5bb70899 100644 --- a/python/vyos/vpp/config_verify.py +++ b/python/vyos/vpp/config_verify.py @@ -103,8 +103,8 @@ def verify_vpp_remove_xconnect_interface(config: dict): for xconn_member, xconn_iface in config.get('xconn_members').items(): if xconn_member == config.get('ifname'): raise ConfigError( - f'interface "{xconn_member}" is still in use within "vpp interfaces xconnect". ' - f'Please remove it from "vpp interface xconnect {xconn_iface}" before proceeding.' + f'interface "{xconn_member}" is still in use within "interfaces vpp xconnect". ' + f'Please remove it from "interface vpp xconnect {xconn_iface}" before proceeding.' ) diff --git a/python/vyos/vpp/interface/__init__.py b/python/vyos/vpp/interface/__init__.py index 0a3176199..3f8ae7954 100644 --- a/python/vyos/vpp/interface/__init__.py +++ b/python/vyos/vpp/interface/__init__.py @@ -19,12 +19,10 @@ from .ethernet import EthernetInterface from .geneve import GeneveInterface from .interface import Interface from .wireguard import WireguardInterface -from .xconnect import XconnectInterface __all__ = [ 'EthernetInterface', 'GeneveInterface', 'Interface', 'WireguardInterface', - 'XconnectInterface', ] diff --git a/python/vyos/vpp/interface/xconnect.py b/python/vyos/vpp/interface/xconnect.py deleted file mode 100644 index dd61d7146..000000000 --- a/python/vyos/vpp/interface/xconnect.py +++ /dev/null @@ -1,95 +0,0 @@ -# VyOS implementation of VPP bridge interface -# -# 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. - -from vyos.vpp import VPPControl -from vyos.vpp.utils import iftunnel_transform - - -class XconnectInterface: - def __init__( - self, - ifname: str, - members: list = [], - state: str = 'up', - ): - self.ifname = ifname - self.members = members - self.vpp = VPPControl() - self.initial_state = state - - def add_l2_xconnect(self): - """Add l2 cross connect - Args: - members (list): The list of the xconnect members - Example: - from vyos.vpp.interface import XconnectInterface - a = XconnectInterface(ifname='xcon0', members=['eth0', 'vxlan0']) - a.add_l2_xconnect() - """ - interface_transform_filter = ('vxlan', 'geneve') - first_member = self.members[0].removeprefix('vpp') - second_member = self.members[1].removeprefix('vpp') - # Check if member in required filter to transform 'vxlanX' => 'vxlan_tunnelX' - if first_member.startswith(interface_transform_filter): - first_member = iftunnel_transform(first_member) - if second_member.startswith(interface_transform_filter): - second_member = iftunnel_transform(second_member) - - member_first_if_index = self.vpp.get_sw_if_index(first_member) - member_second_if_index = self.vpp.get_sw_if_index(second_member) - self.vpp.api.sw_interface_set_l2_xconnect( - rx_sw_if_index=member_first_if_index, - tx_sw_if_index=member_second_if_index, - enable=True, - ) - self.vpp.api.sw_interface_set_l2_xconnect( - rx_sw_if_index=member_second_if_index, - tx_sw_if_index=member_first_if_index, - enable=True, - ) - - def del_l2_xconnect(self): - """Move l2 cross connect member to mode l3 (delte xconnect) - Args: - members (list): The list of the xconnect members - Example: - from vyos.vpp.interface import XconnectInterface - a = XconnectInterface(ifname='xcon0', members=['eth0', 'vxlan0']) - a.del_l2_xconnect() - """ - interface_transform_filter = ('vxlan', 'geneve') - first_member = self.members[0].removeprefix('vpp') - second_member = self.members[1].removeprefix('vpp') - # Check if member in required filter to transform 'vxlanX' => 'vxlan_tunnelX' - if first_member.startswith(interface_transform_filter): - first_member = iftunnel_transform(first_member) - if second_member.startswith(interface_transform_filter): - second_member = iftunnel_transform(second_member) - - member_first_if_index = self.vpp.get_sw_if_index(first_member) - member_second_if_index = self.vpp.get_sw_if_index(second_member) - self.vpp.api.sw_interface_set_l2_xconnect( - rx_sw_if_index=member_first_if_index, - tx_sw_if_index=member_second_if_index, - enable=False, - ) - self.vpp.api.sw_interface_set_l2_xconnect( - rx_sw_if_index=member_second_if_index, - tx_sw_if_index=member_first_if_index, - enable=False, - ) diff --git a/smoketest/scripts/cli/test_vpp.py b/smoketest/scripts/cli/test_vpp.py index 937a6844e..70ac02583 100755 --- a/smoketest/scripts/cli/test_vpp.py +++ b/smoketest/scripts/cli/test_vpp.py @@ -730,9 +730,10 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): self.cli_commit() def test_09_vpp_xconnect(self): + xconn_path = interfaces_path + ['xconnect'] vni = '23' interface_vxlan = f'vppvxlan{vni}' - interface_xconnect = f'xcon{vni}' + interface_xconnect = f'vppxcon{vni}' source_address = '192.0.2.1' remote_address = '192.0.2.254' @@ -748,15 +749,7 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): # Add xconneect self.cli_set( - base_path - + [ - 'interfaces', - 'xconnect', - interface_xconnect, - 'member', - 'interface', - interface, - ] + xconn_path + [interface_xconnect, 'member', 'interface', interface] ) # Cross connect interfaces require 2 interfaces @@ -765,15 +758,7 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): self.cli_commit() self.cli_set( - base_path - + [ - 'interfaces', - 'xconnect', - interface_xconnect, - 'member', - 'interface', - interface_vxlan, - ] + xconn_path + [interface_xconnect, 'member', 'interface', interface_vxlan] ) # commit changes @@ -789,7 +774,7 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): self.assertIn(required_string, out) # delete xconnect interface - self.cli_delete(base_path + ['interfaces', 'xconnect', interface_xconnect]) + self.cli_delete(xconn_path + [interface_xconnect]) self.cli_commit() # check delete xconnect interface diff --git a/src/conf_mode/vpp_interfaces_xconnect.py b/src/conf_mode/vpp_interfaces_xconnect.py index 994332ba2..090d913b0 100644 --- a/src/conf_mode/vpp_interfaces_xconnect.py +++ b/src/conf_mode/vpp_interfaces_xconnect.py @@ -16,12 +16,13 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -import os - from vyos.config import Config from vyos import ConfigError +from vyos.configdict import get_interface_dict +from vyos.utils.process import is_systemd_service_active + +from vyos.ifconfig.vpp import VPPXconnectInterface from vyos.vpp.config_deps import deps_xconnect_dict -from vyos.vpp.interface import XconnectInterface from vyos.vpp.utils import cli_ifaces_list @@ -38,26 +39,15 @@ def get_config(config=None) -> dict: else: conf = Config() - base = ['vpp', 'interfaces', 'xconnect'] - vpp_interfaces = ['vpp', 'settings', 'interface'] + base = ['interfaces', 'vpp', 'xconnect'] - ifname = os.environ['VYOS_TAGNODE_VALUE'] - - # Get config_dict with default values - config = conf.get_config_dict( - base + [ifname], - key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True, - with_defaults=True, - with_recursive_defaults=True, - ) + ifname, config = get_interface_dict(conf, base) - if not conf.exists(['vpp']): + if not conf.exists(['vpp']) and not conf.exists(base): config['remove_vpp'] = True return config - # Get effective config as we need full dicitonary per interface delete + # Get effective config as we need full dictionary per interface delete effective_config = conf.get_config_dict( base + [ifname], key_mangling=('-', '_'), @@ -66,46 +56,42 @@ def get_config(config=None) -> dict: no_tag_node_value_mangle=True, ) - if not config: - config['remove'] = True - if effective_config: config.update({'effective': effective_config}) - # Get global vpp interfaces for verify - config['vpp_interfaces'] = conf.get_config_dict( - vpp_interfaces, - key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True, - ) - config['xconn_members'] = deps_xconnect_dict(conf) config['vpp_ifaces'] = cli_ifaces_list(conf, 'candidate') - config['ifname'] = ifname - return config def verify(config): - if 'remove' in config or 'remove_vpp' in config: + if 'deleted' in config or 'remove_vpp' in config: return None + if not is_systemd_service_active('vpp.service'): + raise ConfigError( + 'Cannot configure layer 2 cross-connect: vpp.service is not running' + ) + # Xconnect requires 2 members if len(config.get('member', {}).get('interface')) != 2: raise ConfigError('Cross connect requires 2 members') - # Member must belong to VPP + not_allowed_prefixes = ('vppbond', 'vppbridge', 'vpplo') for iface in config.get('member', {}).get('interface', []): + # Ensure the interface is allowed as xconnect member + if iface.startswith(not_allowed_prefixes): + raise ConfigError(f'{iface} cannot be configured as xconnect member') + # Member must belong to VPP if iface not in config['vpp_ifaces']: raise ConfigError(f'{iface} must be a VPP interface for xconnect') - # Each interface can belong only to one xconnect - for xconn_member, xconn_ifaces in config['xconn_members'].items(): - if len(xconn_ifaces) > 1: + # Each interface can belong only to one xconnect + xconn_members = config['xconn_members'][iface] + if len(xconn_members) > 1: raise ConfigError( - f'Interface {xconn_member} added to more than one xconnect: {xconn_ifaces}' + f'Interface {iface} added to more than one xconnect: {", ".join(xconn_members)}' ) @@ -118,22 +104,19 @@ def apply(config): return None ifname = config.get('ifname') + xconnect = VPPXconnectInterface(ifname) # Delete xconnect if 'effective' in config: remove_config = config.get('effective') - members = remove_config.get('member', {}).get('interface') - i = XconnectInterface(ifname, members=members) - i.del_l2_xconnect() + members = remove_config['member']['interface'] + xconnect.remove(members) - if 'remove' in config: + if 'deleted' in config: return None # Add xconnect - members = config.get('member', {}).get('interface') - state = 'up' if 'disable' not in config else 'down' - i = XconnectInterface(ifname, members=members, state=state) - i.add_l2_xconnect() + xconnect.update(config) return None diff --git a/src/migration-scripts/vpp/5-to-6 b/src/migration-scripts/vpp/5-to-6 index 0131dee33..a816bb536 100644 --- a/src/migration-scripts/vpp/5-to-6 +++ b/src/migration-scripts/vpp/5-to-6 @@ -49,6 +49,8 @@ # # Migrate bridge interface from vpp section to 'interfaces vpp bridge' (T8327) # +# Migrate xconnect interface from vpp section to 'interfaces vpp xconnect' (T8328) +# from vyos.configtree import ConfigTree @@ -516,6 +518,24 @@ def _migrate_vpp_bridge_interface(config: ConfigTree) -> None: config.delete(['vpp', 'interfaces']) +def _migrate_vpp_xconnect_interface(config: ConfigTree) -> None: + base = ['vpp', 'interfaces', 'xconnect'] + new_base = ['interfaces', 'vpp', 'xconnect'] + + if not config.exists(base): + return + + config.set(new_base) + + for ifname in config.list_nodes(base): + _migrate_interface(config, 'xconnect', ifname) + + config.delete(base) + + if len(config.list_nodes(['vpp', 'interfaces'])) == 0: + config.delete(['vpp', 'interfaces']) + + def migrate(config: ConfigTree) -> None: if not config.exists(['vpp']): # Nothing to do @@ -537,3 +557,4 @@ def migrate(config: ConfigTree) -> None: _migrate_vpp_loopback_interface(config) _migrate_vpp_gre_interface(config) _migrate_vpp_bridge_interface(config) + _migrate_vpp_xconnect_interface(config) |
