summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNataliia Solomko <natalirs1985@gmail.com>2026-03-12 12:05:01 +0200
committerDaniil Baturin <daniil@baturin.org>2026-03-17 20:19:17 +0000
commita37c37da41b26cae0e71e05c260d14bbb1aa1610 (patch)
tree28fc2620fd106b0fc9d5389bf2f9e3b75af71525
parente9f4d103284a456e10fb3776d26b9ff041f9352f (diff)
downloadvyos-1x-a37c37da41b26cae0e71e05c260d14bbb1aa1610.tar.gz
vyos-1x-a37c37da41b26cae0e71e05c260d14bbb1aa1610.zip
vpp: T8230: Add support for PPPoE on bonding interfaces
-rw-r--r--data/config-mode-dependencies/vyos-vpp.json3
-rw-r--r--python/vyos/vpp/control_vpp.py80
-rwxr-xr-xsrc/conf_mode/service_pppoe-server.py47
-rwxr-xr-xsrc/conf_mode/vpp.py10
-rw-r--r--src/conf_mode/vpp_interfaces_bonding.py23
5 files changed, 122 insertions, 41 deletions
diff --git a/data/config-mode-dependencies/vyos-vpp.json b/data/config-mode-dependencies/vyos-vpp.json
index dd664a7c7..af7a8ca0d 100644
--- a/data/config-mode-dependencies/vyos-vpp.json
+++ b/data/config-mode-dependencies/vyos-vpp.json
@@ -21,7 +21,8 @@
"vpp_acl": ["vpp_acl"],
"vpp_nat_nat44": ["vpp_nat_nat44"],
"vpp_nat_cgnat": ["vpp_nat_cgnat"],
- "vpp_ipfix": ["vpp_ipfix"]
+ "vpp_ipfix": ["vpp_ipfix"],
+ "pppoe_server": ["service_pppoe-server"]
},
"vpp_interfaces_gre": {
"vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"],
diff --git a/python/vyos/vpp/control_vpp.py b/python/vyos/vpp/control_vpp.py
index b37bb865f..2daaed423 100644
--- a/python/vyos/vpp/control_vpp.py
+++ b/python/vyos/vpp/control_vpp.py
@@ -15,9 +15,10 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+import re
+
from collections.abc import Callable
from functools import wraps
-from re import search as re_search, MULTILINE as re_M
from systemd import journal
from time import sleep
from typing import TypeVar, ParamSpec, Literal
@@ -330,7 +331,7 @@ class VPPControl:
hw_info = self.cli_cmd(f'show hardware-interfaces {ifname}').reply
regex_filter = r'^\s+pci: device (?P<device>\w+:\w+) subsystem (?P<subsystem>\w+:\w+) address (?P<address>\w+:\w+:\w+\.\w+) numa (?P<numa>\w+)$'
- re_obj = re_search(regex_filter, hw_info, re_M)
+ re_obj = re.search(regex_filter, hw_info, re.MULTILINE)
# return empty string if no interface or no PCI info was found
if not hw_info or not re_obj:
@@ -439,7 +440,6 @@ class VPPControl:
return iface.interface_dev_type
return None
-
@property
def connected(self) -> bool:
"""Check if VPP API is connected
@@ -460,47 +460,89 @@ class VPPControl:
return self.__vpp_api_client.api
@_Decorators.api_call
- def map_pppoe_interface(self, ifname: str, is_add: bool) -> None:
- """Create or delete PPPoE mapping between data-plain and control-plane interfaces
+ def map_pppoe_interface(self, ifname: str) -> None:
+ """
+ Create PPPoE mapping between data-plane and control-plane interfaces.
Args:
- ifname (str): name of an interface in kernel
- is_add (bool): create or delete mapping
+ ifname (str): Name of an interface in kernel.
"""
vpp_pair = self.lcp_pair_find(kernel_name=ifname)
if vpp_pair:
+ vpp_iface_name = vpp_pair['vpp_name_hw']
vpp_pair_name = vpp_pair['vpp_name_kernel']
self.__vpp_api_client.api.pppoe_add_del_cp(
- dp_sw_if_index=self.get_sw_if_index(ifname),
+ dp_sw_if_index=self.get_sw_if_index(vpp_iface_name),
cp_sw_if_index=self.get_sw_if_index(vpp_pair_name),
- is_add=is_add,
+ is_add=True,
)
@_Decorators.api_call
def get_pppoe_interface_mapping(self) -> dict:
"""
- Get PPPoE mapping between data-plain and control-plane interfaces
+ Parse the output of 'show pppoe control-plane binding' to build a mapping
+ between data-plane and control-plane interfaces.
Returns:
- dict: Dict where key is the dataplane interface and value is the control interface
+ dict: Mapping of data-plane interface names/indices to control-plane interface names/indices.
"""
-
reply = self.cli_cmd('show pppoe control-plane binding').reply
+ result = {}
- if 'No PPPoE control plane interface configured' in reply:
- return {}
+ deleted_re = re.compile(r'DELETED\s*\((\d+)\)')
- lines = reply.splitlines()
- lines = lines[2:] # Ignore two lines with headers
+ lines = reply.strip().splitlines()
+
+ # If there are less than 2 lines, assume no PPPoE configured
+ if len(lines) < 2:
+ return result
+
+ # Skip header lines (first 2 lines)
+ for line in lines[2:]:
+ line = line.strip()
+ if not line:
+ continue
+
+ # Split into two columns by 2+ spaces
+ cols = re.split(r'\s{2,}', line, maxsplit=1)
+ if len(cols) != 2:
+ continue
+
+ # Determine key/value depending on which column has "DELETED (index)"
+ col_a, col_b = cols
+
+ match_a = deleted_re.search(col_a)
+ match_b = deleted_re.search(col_b)
+
+ key = int(match_a.group(1)) if match_a else col_a
+ value = int(match_b.group(1)) if match_b else col_b
- result = {}
- for line in lines:
- key, value = line.split()
result[key] = value
return result
@_Decorators.api_call
+ def delete_pppoe_mapping(self, dp_iface: str | int, cp_iface: str | int) -> None:
+ """
+ Delete PPPoE mapping between data-plane and control-plane interfaces.
+
+ Args:
+ dp_iface (str|int): Name or index of an interface in data-plane.
+ cp_iface (str|int): Name or index of an interface in control plane.
+ """
+ dp_index = dp_iface
+ if isinstance(dp_iface, str):
+ dp_index = self.get_sw_if_index(dp_iface)
+ cp_index = cp_iface
+ if isinstance(cp_iface, str):
+ cp_index = self.get_sw_if_index(cp_iface)
+ self.__vpp_api_client.api.pppoe_add_del_cp(
+ dp_sw_if_index=dp_index,
+ cp_sw_if_index=cp_index,
+ is_add=False,
+ )
+
+ @_Decorators.api_call
def enable_dhcp_client(self, ifname: str) -> None:
"""Enable DHCP client detection on a given interface
diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py
index dbcbbabf2..3ec4464c2 100755
--- a/src/conf_mode/service_pppoe-server.py
+++ b/src/conf_mode/service_pppoe-server.py
@@ -69,6 +69,7 @@ def get_config(config=None):
pppoe = get_accel_dict(conf, base, pppoe_chap_secrets)
vpp_interface_base = ['vpp', 'settings', 'interface']
+ vpp_bond_interface_base = ['interfaces', 'vpp', 'bonding']
if conf.exists(vpp_interface_base) and is_systemd_service_active('vpp.service'):
vpp_ifaces = conf.get_config_dict(
vpp_interface_base,
@@ -76,23 +77,23 @@ def get_config(config=None):
get_first_key=True,
no_tag_node_value_mangle=True,
)
+ vpp_bond_ifaces = conf.get_config_dict(
+ vpp_bond_interface_base,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+ vpp_ifaces = vpp_ifaces | vpp_bond_ifaces
pppoe['vpp_ifaces'] = vpp_ifaces
for interface in pppoe.get('interface', {}):
if base_ifname(interface) in vpp_ifaces:
pppoe['interface'][interface]['vpp_cp'] = {}
- pppoe['vpp_cp_interfaces'] = {
- 'add': [
- ifname
- for ifname, iface_conf in pppoe.get('interface', {}).items()
- if 'vpp_cp' in iface_conf
- ],
- 'delete': [
- iface
- for iface in node_changed(conf, base + ['interface'])
- if base_ifname(iface) in pppoe.get('vpp_ifaces', {})
- ],
- }
+ pppoe['vpp_cp_interfaces'] = [
+ ifname
+ for ifname, iface_conf in pppoe.get('interface', {}).items()
+ if 'vpp_cp' in iface_conf
+ ]
if not conf.exists(base):
pppoe['remove'] = True
@@ -112,6 +113,13 @@ def get_config(config=None):
changed_vpp_ifaces = node_changed(
conf, vpp_interface_base, expand_nodes=Diff.DELETE | Diff.ADD
)
+ changed_vpp_bond_ifaces = node_changed(
+ conf,
+ vpp_bond_interface_base,
+ recursive=True,
+ expand_nodes=Diff.DELETE | Diff.ADD,
+ )
+ all_changed_vpp_ifaces = set(changed_vpp_ifaces) | set(changed_vpp_bond_ifaces)
conditions = [
is_node_changed(conf, base + ['client-ip-pool']),
is_node_changed(conf, base + ['client-ipv6-pool']),
@@ -119,7 +127,7 @@ def get_config(config=None):
is_node_changed(conf, base + ['authentication', 'radius', 'dynamic-author']),
is_node_changed(conf, base + ['authentication', 'mode']),
any(
- base_ifname(iface) in changed_vpp_ifaces
+ base_ifname(iface) in all_changed_vpp_ifaces
for iface in pppoe.get('interface', {})
),
]
@@ -208,14 +216,11 @@ def apply(pppoe):
systemd_service = 'accel-ppp@pppoe.service'
# delete pppoe mapping in vpp
- vpp_cp_ifaces_delete = pppoe.get('vpp_cp_interfaces', {}).get('delete', [])
- if 'vpp_ifaces' in pppoe and vpp_cp_ifaces_delete:
+ if 'vpp_ifaces' in pppoe:
vpp = VPPControl()
- # Make sure that the mapping really exists
mapping = vpp.get_pppoe_interface_mapping()
- for iface in vpp_cp_ifaces_delete:
- if iface in mapping:
- vpp.map_pppoe_interface(iface, is_add=False)
+ for dp_iface, cp_iface in mapping.items():
+ vpp.delete_pppoe_mapping(dp_iface, cp_iface)
if 'remove' in pppoe:
call(f'systemctl stop {systemd_service}')
@@ -230,11 +235,11 @@ def apply(pppoe):
call(f'systemctl reload-or-restart {systemd_service}')
# add pppoe mapping in vpp
- vpp_cp_ifaces_add = pppoe.get('vpp_cp_interfaces', {}).get('add', [])
+ vpp_cp_ifaces_add = pppoe.get('vpp_cp_interfaces', [])
if vpp_cp_ifaces_add:
vpp = VPPControl()
for iface in vpp_cp_ifaces_add:
- vpp.map_pppoe_interface(iface, is_add=True)
+ vpp.map_pppoe_interface(iface)
if __name__ == '__main__':
diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py
index 22cf04020..a45d4dc65 100755
--- a/src/conf_mode/vpp.py
+++ b/src/conf_mode/vpp.py
@@ -303,6 +303,7 @@ def get_config(config=None):
'bond_members': bond_members,
'persist_config': eth_ifaces_persist,
'interfaces_vpp': interfaces_config,
+ 'pppoe_ifaces': pppoe_ifaces,
'remove': {},
}
@@ -503,6 +504,15 @@ def verify(config):
'VPP cannot be removed while VPP interfaces exist. Remove all "interfaces vpp" first!'
)
+ # Find PPPoE ifaces where the base matches any VPP interface (base or VLAN)
+ pppoe_vpp_ifaces = [
+ iface for iface in config.get('pppoe_ifaces', {}) if iface.startswith('vpp')
+ ]
+ if 'remove' in config and pppoe_vpp_ifaces:
+ raise ConfigError(
+ f'Cannot remove VPP: PPPoE server still uses VPP interface(s): {", ".join(pppoe_vpp_ifaces)}'
+ )
+
# bail out early - looks like removal from running config
if not config or 'remove' in config:
return None
diff --git a/src/conf_mode/vpp_interfaces_bonding.py b/src/conf_mode/vpp_interfaces_bonding.py
index 582b82d99..8d9930a20 100644
--- a/src/conf_mode/vpp_interfaces_bonding.py
+++ b/src/conf_mode/vpp_interfaces_bonding.py
@@ -73,6 +73,9 @@ def get_config(config=None) -> dict:
ifname, config = get_interface_dict(conf, base)
+ # Get pppoe-server interfaces
+ config['pppoe_ifaces'] = conf.list_nodes(['service', 'pppoe-server', 'interface'])
+
if not conf.exists(['vpp']) and not conf.exists(base):
config['remove_vpp'] = True
return config
@@ -106,6 +109,10 @@ def get_config(config=None) -> dict:
for bridge_iface in config['bridge_members'][ifname]:
set_dependents('vpp_interfaces_bridge', conf, bridge_iface)
+ # PPPoE dependency
+ if any(i == ifname or i.startswith(f'{ifname}.') for i in config['pppoe_ifaces']):
+ set_dependents('pppoe_server', conf)
+
# NAT dependency
if conf.exists(['vpp', 'nat', 'nat44']):
set_dependents('vpp_nat_nat44', conf)
@@ -124,6 +131,15 @@ def get_config(config=None) -> dict:
def verify(config):
+ ifname = config['ifname']
+ if 'deleted' in config and any(
+ i == ifname or i.startswith(f'{ifname}.')
+ for i in config.get('pppoe_ifaces', [])
+ ):
+ raise ConfigError(
+ 'Cannot remove interface: it is still in use by the PPPoE server'
+ )
+
if 'remove_vpp' in config:
return None
@@ -167,6 +183,13 @@ def verify(config):
f'Cannot use {mac}: it is a multicast MAC address. Please provide a unicast MAC address.'
)
+ for vif_remove in config.get('vif_remove', []):
+ vif_iface = f'{ifname}.{vif_remove}'
+ if vif_iface in config.get('pppoe_ifaces', []):
+ raise ConfigError(
+ f'Cannot remove interface {vif_iface}: it is still in use by the PPPoE server'
+ )
+
def generate(config):
pass