diff options
| author | Nataliia Solomko <natalirs1985@gmail.com> | 2025-09-15 19:12:04 +0300 |
|---|---|---|
| committer | Nataliia Solomko <natalirs1985@gmail.com> | 2025-09-17 17:49:45 +0300 |
| commit | 03ded6c071b9bfee98c7ccf3983be56f0ad98c4f (patch) | |
| tree | fb4a131462fb3845982f9aa5c2158a07798368a5 | |
| parent | 4669748ca1b39dc0cb54496d5ce695d05c8431f7 (diff) | |
| download | vyos-1x-03ded6c071b9bfee98c7ccf3983be56f0ad98c4f.tar.gz vyos-1x-03ded6c071b9bfee98c7ccf3983be56f0ad98c4f.zip | |
T7796: PPPoE-server add mapping in vpp if vpp-cp is enabled
| -rw-r--r-- | data/config-mode-dependencies/vyos-vpp.json | 3 | ||||
| -rw-r--r-- | python/vyos/vpp/control_vpp.py | 17 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_service_pppoe-server.py | 15 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_vpp.py | 50 | ||||
| -rwxr-xr-x | src/conf_mode/service_pppoe-server.py | 73 | ||||
| -rwxr-xr-x | src/conf_mode/vpp.py | 33 |
6 files changed, 177 insertions, 14 deletions
diff --git a/data/config-mode-dependencies/vyos-vpp.json b/data/config-mode-dependencies/vyos-vpp.json index 32b53508e..4729ae95c 100644 --- a/data/config-mode-dependencies/vyos-vpp.json +++ b/data/config-mode-dependencies/vyos-vpp.json @@ -14,7 +14,8 @@ "vpp_nat": ["vpp_nat"], "vpp_nat_cgnat": ["vpp_nat_cgnat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"], - "vpp_sflow": ["vpp_sflow"] + "vpp_sflow": ["vpp_sflow"], + "pppoe_server": ["service_pppoe-server"] }, "vpp_interfaces_bonding": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], diff --git a/python/vyos/vpp/control_vpp.py b/python/vyos/vpp/control_vpp.py index 596ef4f12..42e7bf6c1 100644 --- a/python/vyos/vpp/control_vpp.py +++ b/python/vyos/vpp/control_vpp.py @@ -486,3 +486,20 @@ class VPPControl: Callable[AnyParam, AnyType]: API functions """ 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 + + Args: + ifname (str): name of an interface in kernel + is_add (bool): create or delete mapping + """ + vpp_pair = self.lcp_pair_find(kernel_name=ifname) + if vpp_pair: + 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), + cp_sw_if_index=self.get_sw_if_index(vpp_pair_name), + is_add=is_add, + ) diff --git a/smoketest/scripts/cli/test_service_pppoe-server.py b/smoketest/scripts/cli/test_service_pppoe-server.py index 609fb7aaa..8a6886cb9 100755 --- a/smoketest/scripts/cli/test_service_pppoe-server.py +++ b/smoketest/scripts/cli/test_service_pppoe-server.py @@ -212,18 +212,17 @@ class TestServicePPPoEServer(BasicAccelPPPTest.TestCase): self.assertIn('accept-blank-service=1', config) def test_accel_vpp_cp(self): - interface_vpp = f'{interface},vpp-cp=true' - self.basic_config() - self.set(['interface', interface, 'vpp-cp']) self.cli_commit() - # Validate configuration values - conf = ConfigParser(allow_no_value=True, delimiters='=') - conf.read(self._config_file) + self.set(['interface', interface, 'vpp-cp']) - # Validate configuration - self.assertEqual(conf['pppoe']['interface'], interface_vpp) + # vpp-cp require VPP service to be started and interface is configured in VPP + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + # All other checks for PPPoE control-plane integration + # with VPP are verified in test_vpp.py if __name__ == '__main__': diff --git a/smoketest/scripts/cli/test_vpp.py b/smoketest/scripts/cli/test_vpp.py index b55db9e49..0611d2851 100755 --- a/smoketest/scripts/cli/test_vpp.py +++ b/smoketest/scripts/cli/test_vpp.py @@ -1445,6 +1445,56 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): self.assertEqual(sysctl_read('vm.max_map_count'), '65530') self.assertEqual(sysctl_read('kernel.shmmax'), '8589934592') + def test_20_vpp_pppoe_mapping(self): + config_file = '/run/accel-pppd/pppoe.conf' + pool = "TEST-POOL" + vni = '23' + pppoe_base = ['service', 'pppoe-server'] + + self.cli_set(['interfaces', 'ethernet', interface, 'vif', vni]) + + # Basic pppoe-server config + self.cli_set(pppoe_base + ['authentication', 'mode', 'noauth']) + self.cli_set(pppoe_base + ['gateway-address', '192.0.2.1']) + self.cli_set(pppoe_base + ['client-ip-pool', pool, 'range', '192.0.2.0/24']) + self.cli_set(pppoe_base + ['default-pool', pool]) + + # Enable PPPoE control-plane integration with VPP + self.cli_set(pppoe_base + ['interface', interface, 'vpp-cp']) + self.cli_set(pppoe_base + ['interface', f'{interface}.{vni}', 'vpp-cp']) + + self.cli_commit() + + # Validate configuration values + config = read_file(config_file) + + # Validate configuration + self.assertIn(f'interface={interface},vpp-cp=true', config) + self.assertIn(f'interface={interface}.{vni},vpp-cp=true', config) + + # Check pppoe mapping + _, out = rc_cmd('sudo vppctl show pppoe control-plane binding') + self.assertRegex(out, rf'{interface}\s+tap4096') + self.assertRegex(out, rf'{interface}.{vni}\s+tap4096.23') + + # check if dependency is called and mapping is correct after changes in vpp script + self.cli_set( + base_path + ['settings', 'interface', interface, 'dpdk-options', 'promisc'] + ) + self.cli_commit() + + # Check pppoe mapping + _, out = rc_cmd('sudo vppctl show pppoe control-plane binding') + self.assertRegex(out, rf'{interface}\s+tap4096') + self.assertRegex(out, rf'{interface}.{vni}\s+tap4096.23') + + # delete PPPoE config + self.cli_delete(pppoe_base) + + # delete vif Ethernet interface + self.cli_delete(['interfaces', 'ethernet', interface, 'vif']) + self.cli_commit() + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index d910e770d..bfa475b95 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -21,6 +21,7 @@ from sys import exit from vyos.config import Config from vyos.configdict import get_accel_dict from vyos.configdict import is_node_changed +from vyos.configdiff import get_config_diff, Diff from vyos.configverify import verify_interface_exists from vyos.template import render from vyos.utils.process import call @@ -32,6 +33,7 @@ from vyos.accel_ppp_util import verify_accel_ppp_ip_pool from vyos.accel_ppp_util import get_pools_in_order from vyos import ConfigError from vyos import airbag +from vyos.vpp.control_vpp import VPPControl airbag.enable() @@ -54,12 +56,40 @@ def get_config(config=None): else: conf = Config() base = ['service', 'pppoe-server'] - if not conf.exists(base): - return None # retrieve common dictionary keys pppoe = get_accel_dict(conf, base, pppoe_chap_secrets) + if conf.exists(['vpp']): + pppoe['vpp_config'] = conf.get_config_dict( + ['vpp'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + diff = get_config_diff(conf) + node_diff = diff.get_child_nodes_diff( + base + ['interface'], expand_nodes=Diff.DELETE, recursive=True + ) + + 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, iface_config in node_diff.get('delete').items() + if 'vpp-cp' in iface_config + ], + } + + if not conf.exists(base): + pppoe['remove'] = True + return pppoe + if dict_search('client_ip_pool', pppoe): # Multiple named pools require ordered values T5099 pppoe['ordered_named_pools'] = get_pools_in_order(dict_search('client_ip_pool', pppoe)) @@ -110,7 +140,7 @@ def verify_pado_delay(pppoe): ) def verify(pppoe): - if not pppoe: + if 'remove' in pppoe: return None verify_accel_ppp_authentication(pppoe) @@ -129,11 +159,28 @@ def verify(pppoe): if 'vlan_mon' in interface_config and not 'vlan' in interface_config: raise ConfigError('Option "vlan-mon" requires "vlan" to be set!') + vpp_cp_interfaces = pppoe.get('vpp_cp_interfaces', {}).get('add', []) + if vpp_cp_interfaces: + try: + vpp = VPPControl() + except Exception: + raise ConfigError( + 'PPPoE control-plane integration with VPP is enabled on ' + f'interface(s) {", ".join(vpp_cp_interfaces)} ' + 'but VPP service was not started' + ) + + for iface in vpp_cp_interfaces: + if vpp.get_sw_if_index(iface) is None: + raise ConfigError( + f'{iface} should be a VPP interface for control-plane integration' + ) + return None def generate(pppoe): - if not pppoe: + if 'remove' in pppoe: return None render(pppoe_conf, 'accel-ppp/pppoe.config.j2', pppoe) @@ -146,7 +193,15 @@ def generate(pppoe): def apply(pppoe): systemd_service = 'accel-ppp@pppoe.service' - if not pppoe: + + # delete pppoe mapping in vpp + vpp_cp_ifaces_delete = pppoe.get('vpp_cp_interfaces', {}).get('delete', []) + if 'vpp_config' in pppoe and vpp_cp_ifaces_delete: + vpp = VPPControl() + for iface in vpp_cp_ifaces_delete: + vpp.map_pppoe_interface(iface, is_add=False) + + if 'remove' in pppoe: call(f'systemctl stop {systemd_service}') for file in [pppoe_conf, pppoe_chap_secrets]: if os.path.exists(file): @@ -158,6 +213,14 @@ def apply(pppoe): else: call(f'systemctl reload-or-restart {systemd_service}') + # add pppoe mapping in vpp + vpp_cp_ifaces_add = pppoe.get('vpp_cp_interfaces', {}).get('add', []) + if vpp_cp_ifaces_add: + vpp = VPPControl() + for iface in vpp_cp_ifaces_add: + vpp.map_pppoe_interface(iface, is_add=True) + + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index dc9ba1507..492fa92da 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -229,11 +229,25 @@ def get_config(config=None): if conf.exists(['vpp', 'acl']): set_dependents('vpp_acl', conf) + # Get interfaces that are used in PPPoe for control-plane integration + pppoe_conf = conf.get_config_dict( + ['service', 'pppoe-server'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + pppoe_map_ifaces = [ + ifname + for ifname, iface_conf in pppoe_conf.get('interface', {}).items() + if 'vpp_cp' in iface_conf + ] + if not conf.exists(base): return { 'removed_ifaces': removed_ifaces, 'xconn_members': xconn_members, 'persist_config': eth_ifaces_persist, + **({'pppoe_ifaces': pppoe_map_ifaces} if pppoe_map_ifaces else {}), } config = conf.get_config_dict( @@ -372,6 +386,11 @@ def get_config(config=None): eth_ifaces_persist[iface]['bus_id'] = control_host.get_bus_name(iface) eth_ifaces_persist[iface]['dev_id'] = control_host.get_dev_id(iface) + # PPPoE dependency + if pppoe_map_ifaces: + config['pppoe_ifaces'] = pppoe_map_ifaces + set_dependents('pppoe_server', conf) + # Return to config dictionary config['persist_config'] = eth_ifaces_persist @@ -379,6 +398,20 @@ def get_config(config=None): def verify(config): + # Cannot remove interface if PPPoE control-plane is still enabled + removed_ifaces = [iface['iface_name'] for iface in config.get('removed_ifaces', [])] + pppoe_removed_ifaces = [ + p + for p in config.get('pppoe_ifaces', []) + for r in removed_ifaces + if p == r or p.startswith(f'{r}.') + ] + if pppoe_removed_ifaces: + raise ConfigError( + f'{", ".join(pppoe_removed_ifaces)} still in use by the PPPoE server. ' + 'Disable PPPoE control-plane integration with VPP before proceeding.' + ) + # bail out early - looks like removal from running config if not config or ('removed_ifaces' in config and 'settings' not in config): return None |
