summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNataliia Solomko <natalirs1985@gmail.com>2025-09-15 19:12:04 +0300
committerNataliia Solomko <natalirs1985@gmail.com>2025-09-17 17:49:45 +0300
commit03ded6c071b9bfee98c7ccf3983be56f0ad98c4f (patch)
treefb4a131462fb3845982f9aa5c2158a07798368a5
parent4669748ca1b39dc0cb54496d5ce695d05c8431f7 (diff)
downloadvyos-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.json3
-rw-r--r--python/vyos/vpp/control_vpp.py17
-rwxr-xr-xsmoketest/scripts/cli/test_service_pppoe-server.py15
-rwxr-xr-xsmoketest/scripts/cli/test_vpp.py50
-rwxr-xr-xsrc/conf_mode/service_pppoe-server.py73
-rwxr-xr-xsrc/conf_mode/vpp.py33
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