From e4dd18c806c3e60553fe34e7c8a22b8ec7824888 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Mon, 30 Oct 2023 17:26:26 +0100 Subject: [PATCH] Implement trunk extension Implement the trunk service extension. A trunk can be created on any port that is a in a direct binding hostgroup. Only one trunk per hostgroup is possible. If the host is part of a metagroup, only VLANs not used by the metagroup VLAN pool can be used as target, to not interfere with VMs put on the host(s). The trunk plugin modifies the subports by setting binding host, vnic type and binding profile. The binding profile contains details about the specific VLAN translation, but this is only informational, as the "real" info will be fetched from the respective trunk/subport tables in the DB. --- .../common/config/config_driver.py | 10 +++ networking_ccloud/db/db_plugin.py | 11 ++- networking_ccloud/ml2/mech_driver.py | 12 ++- networking_ccloud/ml2/plugin.py | 5 +- .../tests/unit/common/test_driver_config.py | 25 ++++++ .../tests/unit/db/test_db_plugin.py | 14 ++++ .../tests/unit/ml2/test_mech_driver.py | 76 ++++++++++++++++++- 7 files changed, 143 insertions(+), 10 deletions(-) diff --git a/networking_ccloud/common/config/config_driver.py b/networking_ccloud/common/config/config_driver.py index c6a7beb0..484c2977 100644 --- a/networking_ccloud/common/config/config_driver.py +++ b/networking_ccloud/common/config/config_driver.py @@ -456,6 +456,16 @@ def has_switches_as_member(self, drv_conf, switch_names): return True return False + def get_parent_metagroup(self, drv_conf): + """Get metagroup for this Hostgroup, if it is part of a metagroup""" + if self.metagroup: + return None + + for hg in drv_conf.hostgroups: + if any(host in hg.members for host in self.binding_hosts): + return hg + return None + class VRF(pydantic.BaseModel): name: str diff --git a/networking_ccloud/db/db_plugin.py b/networking_ccloud/db/db_plugin.py index ef0f290b..3c1aa3d7 100644 --- a/networking_ccloud/db/db_plugin.py +++ b/networking_ccloud/db/db_plugin.py @@ -103,8 +103,6 @@ def get_hosts_on_segments(self, context, segment_ids=None, network_ids=None, phy hosts = net_hosts.setdefault(network_id, {}) if host not in hosts: - # FIXME: do we want to take the trunk segmentation id from the SubPort table - # or alternatively from the port's binding profile? hosts[host] = dict(segment_id=segment_id, network_id=network_id, segmentation_id=segmentation_id, physical_network=physnet, driver=driver, level=level, trunk_segmentation_id=trunk_seg_id, is_bgw=False) @@ -401,3 +399,12 @@ def get_subnetpool_details(self, context, subnetpool_ids): result[snp_id]['cidrs'].append(cidr) return result + + @db_api.retry_if_session_inactive() + def get_subport_trunk_vlan_id(self, context, port_id): + query = context.session.query(trunk_models.SubPort.segmentation_id) + query = query.filter(trunk_models.SubPort.port_id == port_id) + subport = query.first() + if subport: + return subport.segmentation_id + return None diff --git a/networking_ccloud/ml2/mech_driver.py b/networking_ccloud/ml2/mech_driver.py index b3d9ce09..4e757bca 100644 --- a/networking_ccloud/ml2/mech_driver.py +++ b/networking_ccloud/ml2/mech_driver.py @@ -22,6 +22,7 @@ from neutron_lib.plugins import directory from neutron_lib.plugins.ml2 import api as ml2_api from neutron_lib import rpc as n_rpc +from neutron_lib.services.trunk import constants as trunk_const from oslo_config import cfg from oslo_log import log as logging @@ -33,6 +34,7 @@ from networking_ccloud.ml2.agent.common import messages as agent_msg from networking_ccloud.ml2.driver_rpc_api import CCFabricDriverAPI from networking_ccloud.ml2.plugin import FabricPlugin +from networking_ccloud.services.trunk.driver import CCTrunkDriver LOG = logging.getLogger(__name__) @@ -85,6 +87,7 @@ def initialize(self): self._agents = {} fabricoperations.register_api_extension() + self.trunk_driver = CCTrunkDriver.create() LOG.info("CC-Fabric ml2 driver initialized") @@ -235,11 +238,16 @@ def _bind_port_direct(self, context, binding_host, hg_config): context.current['id'], config_physnet, context.segments_to_bind) return - # FIXME: trunk ports + trunk_vlan = None + if context.current['device_owner'] == trunk_const.TRUNK_SUBPORT_OWNER: + if hg_config.direct_binding and not hg_config.role: + trunk_vlan = self.fabric_plugin.get_subport_trunk_vlan_id(context._plugin_context, + context.current['id']) + net_external = context.network.current[extnet_api.EXTERNAL] self.handle_binding_host_changed(context._plugin_context, context.current['network_id'], binding_host, hg_config, context.binding_levels[0][ml2_api.BOUND_SEGMENT], segment, - net_external=net_external) + net_external=net_external, trunk_vlan=trunk_vlan) vif_details = {} # no vif-details needed yet context.set_binding(segment['id'], cc_const.VIF_TYPE_CC_FABRIC, vif_details, nl_const.ACTIVE) diff --git a/networking_ccloud/ml2/plugin.py b/networking_ccloud/ml2/plugin.py index a73aa9bf..d1afd480 100644 --- a/networking_ccloud/ml2/plugin.py +++ b/networking_ccloud/ml2/plugin.py @@ -95,6 +95,7 @@ def allocate_and_configure_interconnects(self, context, network): device_segment = self._plugin.type_manager.allocate_dynamic_segment(context, network_id, segment_spec) device_segment['is_bgw'] = device_type == cc_const.DEVICE_TYPE_BGW + device_segment['trunk_segmentation_id'] = None self.add_segments_to_config(context, scul, {network_id: {device.host: device_segment}}) if device_type == cc_const.DEVICE_TYPE_TRANSIT: @@ -173,11 +174,11 @@ def add_segments_to_config(self, context, scul, net_segments): LOG.error("Got a port binding for binding host %s in network %s, which was not found in config", binding_host, network_id) continue - # FIXME: handle trunk_vlans + trunk_vlan = segment_1['trunk_segmentation_id'] # FIXME: exclude_hosts # FIXME: direct binding hosts? are they included? gateways = net_gateways.get(network_id) - scul.add_binding_host_to_config(hg_config, network_id, vni, vlan, + scul.add_binding_host_to_config(hg_config, network_id, vni, vlan, trunk_vlan, gateways=gateways, is_bgw=segment_1['is_bgw']) if gateways: l3_net_switch_map.setdefault(network_id, set()).update(hg_config.get_switch_names(self.drv_conf)) diff --git a/networking_ccloud/tests/unit/common/test_driver_config.py b/networking_ccloud/tests/unit/common/test_driver_config.py index 5a42cac9..6bf1ca7d 100644 --- a/networking_ccloud/tests/unit/common/test_driver_config.py +++ b/networking_ccloud/tests/unit/common/test_driver_config.py @@ -206,6 +206,31 @@ def test_duplicate_vrf_id(self): self.assertRaisesRegex(ValueError, "VRF id 2 is duplicated on VRF SWITCH-ME", cfix.make_global_config, cfix.make_azs(['monster-az-a']), vrfs=vrfs) + def test_get_metagroup_for_child_hostgroup(self): + # should work + gc = cfix.make_global_config(availability_zones=cfix.make_azs(["qa-de-1a", "qa-de-1b"])) + sg1 = cfix.make_switchgroup("seagull", availability_zone="qa-de-1a") + sg2 = cfix.make_switchgroup("crow", availability_zone="qa-de-1a") + hg_seagulls = cfix.make_metagroup("seagull") + hg_crows = cfix.make_hostgroups("crow") + drv_conf = config.DriverConfig(global_config=gc, switchgroups=[sg1, sg2], hostgroups=hg_seagulls + hg_crows) + + # metagroups have no parent + parent_hg = drv_conf.get_hostgroup_by_host("nova-compute-seagull") + self.assertIsNotNone(parent_hg) + self.assertIsNone(parent_hg.get_parent_metagroup(drv_conf)) + + # children are part of their metagroup + child_hg = drv_conf.get_hostgroup_by_host("node002-seagull") + self.assertIsNotNone(child_hg) + self.assertEqual(parent_hg, child_hg.get_parent_metagroup(drv_conf)) + + # crow is not part of any metagroup + crow_hg = drv_conf.get_hostgroup_by_host("node003-crow") + self.assertIsNotNone(crow_hg) + self.assertTrue(crow_hg.direct_binding) + self.assertIsNone(crow_hg.get_parent_metagroup(drv_conf)) + class TestDriverConfig(base.TestCase): def setUp(self): diff --git a/networking_ccloud/tests/unit/db/test_db_plugin.py b/networking_ccloud/tests/unit/db/test_db_plugin.py index 54187169..e0bb3c00 100644 --- a/networking_ccloud/tests/unit/db/test_db_plugin.py +++ b/networking_ccloud/tests/unit/db/test_db_plugin.py @@ -304,6 +304,20 @@ def test_get_subnetpool_details(self): }}, self._db.get_subnetpool_details(ctx, [self._subnetpool_reg['id'], self._subnetpool_az['id']])) + def test_get_subport_trunk_vlan_id(self): + ctx = context.get_admin_context() + with self.port() as trunkport, self.port() as subport: + self.assertIsNone(self._db.get_subport_trunk_vlan_id(ctx, subport['port']['id'])) + + with ctx.session.begin(): + subport = trunk_models.SubPort(port_id=subport['port']['id'], segmentation_type='vlan', + segmentation_id=1000) + trunk = trunk_models.Trunk(name='random-trunk', port_id=trunkport['port']['id'], sub_ports=[subport]) + ctx.session.add(trunk) + + self.assertEqual(1000, self._db.get_subport_trunk_vlan_id(ctx, subport['port']['id'])) + self.assertIsNone(self._db.get_subport_trunk_vlan_id(ctx, trunkport['port']['id'])) + class TestNetworkInterconnectAllocation(test_segment.SegmentTestCase, base.PortBindingHelper, base.TestCase): def setUp(self): diff --git a/networking_ccloud/tests/unit/ml2/test_mech_driver.py b/networking_ccloud/tests/unit/ml2/test_mech_driver.py index ee514d69..4e68c876 100644 --- a/networking_ccloud/tests/unit/ml2/test_mech_driver.py +++ b/networking_ccloud/tests/unit/ml2/test_mech_driver.py @@ -23,6 +23,8 @@ from neutron.db import segments_db from neutron.plugins.ml2 import driver_context from neutron.plugins.ml2 import models as ml2_models +from neutron.services.trunk import models as trunk_models +from neutron.services.trunk import plugin as trunk_plugin from neutron.tests.common import helpers as neutron_test_helpers from neutron.tests.unit.plugins.ml2 import test_plugin @@ -33,6 +35,7 @@ from neutron_lib import context from neutron_lib import exceptions as nl_exc from neutron_lib.plugins import directory +from neutron_lib.services.trunk import constants as trunk_const from oslo_config import cfg from networking_ccloud.common.config import _override_driver_config @@ -55,19 +58,25 @@ def _register_azs(self): self.agent2 = neutron_test_helpers.register_dhcp_agent(host='network-agent-b-1', az='qa-de-1b') self.agent3 = neutron_test_helpers.register_dhcp_agent(host='network-agent-c-1', az='qa-de-1c') - def _test_bind_port(self, fake_host, fake_segments=None, network=None, subnet=None, binding_levels=None): + def _test_bind_port(self, fake_host, fake_segments=None, network=None, subnet=None, binding_levels=None, + port_extra={}, port_created_cb=None): if network is None: with self.network() as network: - return self._test_bind_port(fake_host, fake_segments, network, binding_levels=binding_levels) + return self._test_bind_port(fake_host, fake_segments, network, binding_levels=binding_levels, + port_extra=port_extra, port_created_cb=port_created_cb) if subnet is None: with self.subnet(network=network) as subnet: - return self._test_bind_port(fake_host, fake_segments, network, subnet, binding_levels) + return self._test_bind_port(fake_host, fake_segments, network, subnet, binding_levels, + port_extra=port_extra, port_created_cb=port_created_cb) - with self.port(subnet=subnet) as port: + with self.port(subnet=subnet, network=network, **port_extra) as port: port['port']['binding:host_id'] = fake_host if fake_segments is None: fake_segments = [self._vxlan_segment] + if port_created_cb: + port_created_cb(port=port, subnet=subnet, network=network) + with mock.patch('neutron.plugins.ml2.driver_context.PortContext.binding_levels', new_callable=mock.PropertyMock) as bl_mock: bindings = ml2_models.PortBinding() @@ -169,6 +178,65 @@ def test_bind_port_direct_level_1_broken_segment(self): mock_bhc.assert_not_called() context.set_binding.assert_not_called() + def test_bind_port_trunking_direct_level_1(self): + ctx = context.get_admin_context() + + fake_segments = [{'id': 'fake-segment-id', 'physical_network': 'seagull', 'segmentation_id': 42, + 'network_type': 'vlan'}] + binding_levels = [{'driver': 'cc-fabric', 'bound_segment': self._vxlan_segment}] + + with mock.patch.object(CCFabricSwitchAgentRPCClient, 'apply_config_update') as mock_acu: + def _create_trunk(port, network, **kwargs): + with ctx.session.begin(): + # FIXME: right port id + subport = trunk_models.SubPort(port_id=port['port']['id'], segmentation_type='vlan', + segmentation_id=1234) + trunk_port = self._make_port(net_id=network['network']['id'], fmt="json") + trunk = trunk_models.Trunk(name='random-trunk', port_id=trunk_port['port']['id'], + sub_ports=[subport]) + ctx.session.add(trunk) + p_context = self._test_bind_port(fake_host='node001-seagull', + fake_segments=fake_segments, binding_levels=binding_levels, + port_extra={'device_owner': trunk_const.TRUNK_SUBPORT_OWNER}, + port_created_cb=_create_trunk) + p_context.continue_binding.assert_not_called() + mock_acu.assert_called() + p_context.set_binding.assert_called() + + # check config + swcfgs = mock_acu.call_args[0][1] + self.assertEqual(2, len(swcfgs)) + for swcfg in swcfgs: + self.assertEqual(agent_msg.OperationEnum.add, swcfg.operation) + self.assertTrue(swcfg.switch_name.startswith("seagull-sw")) + self.assertEqual((23, 42), (swcfg.vxlan_maps[0].vni, swcfg.vxlan_maps[0].vlan)) + self.assertIsNone(swcfg.ifaces[0].native_vlan) + self.assertEqual([42], swcfg.ifaces[0].trunk_vlans) + self.assertEqual(1, len(swcfg.ifaces[0].vlan_translations)) + self.assertEqual({'inside': 42, 'outside': 1234}, swcfg.ifaces[0].vlan_translations[0].dict()) + + def test_bind_port_trunking_without_subport_direct_level_1(self): + fake_segments = [{'id': 'fake-segment-id', 'physical_network': 'seagull', 'segmentation_id': 42, + 'network_type': 'vlan'}] + binding_levels = [{'driver': 'cc-fabric', 'bound_segment': self._vxlan_segment}] + with mock.patch.object(CCFabricSwitchAgentRPCClient, 'apply_config_update') as mock_acu: + context = self._test_bind_port(fake_host='node001-seagull', + fake_segments=fake_segments, binding_levels=binding_levels, + port_extra={'device_owner': trunk_const.TRUNK_SUBPORT_OWNER}) + context.continue_binding.assert_not_called() + mock_acu.assert_called() + context.set_binding.assert_called() + + # check config + swcfgs = mock_acu.call_args[0][1] + self.assertEqual(2, len(swcfgs)) + for swcfg in swcfgs: + self.assertEqual(agent_msg.OperationEnum.add, swcfg.operation) + self.assertTrue(swcfg.switch_name.startswith("seagull-sw")) + self.assertEqual((23, 42), (swcfg.vxlan_maps[0].vni, swcfg.vxlan_maps[0].vlan)) + self.assertEqual(42, swcfg.ifaces[0].native_vlan) + self.assertEqual([42], swcfg.ifaces[0].trunk_vlans) + def test_bind_port_hpb(self): # only one stage bound with mock.patch.object(CCFabricSwitchAgentRPCClient, 'apply_config_update') as mock_acu: