diff options
| author | Nataliia Solomko <natalirs1985@gmail.com> | 2026-03-12 12:05:01 +0200 |
|---|---|---|
| committer | Daniil Baturin <daniil@baturin.org> | 2026-03-17 20:19:17 +0000 |
| commit | a37c37da41b26cae0e71e05c260d14bbb1aa1610 (patch) | |
| tree | 28fc2620fd106b0fc9d5389bf2f9e3b75af71525 | |
| parent | e9f4d103284a456e10fb3776d26b9ff041f9352f (diff) | |
| download | vyos-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.json | 3 | ||||
| -rw-r--r-- | python/vyos/vpp/control_vpp.py | 80 | ||||
| -rwxr-xr-x | src/conf_mode/service_pppoe-server.py | 47 | ||||
| -rwxr-xr-x | src/conf_mode/vpp.py | 10 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_bonding.py | 23 |
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 |
