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/common/constants.py b/networking_ccloud/common/constants.py index 9a0081f7..dcaa2d0b 100644 --- a/networking_ccloud/common/constants.py +++ b/networking_ccloud/common/constants.py @@ -42,3 +42,4 @@ PLATFORM_EOS: SWITCH_AGENT_EOS_TOPIC, PLATFORM_NXOS: SWITCH_AGENT_NXOS_TOPIC, } +TRUNK_PROFILE = 'cc-fabric_trunk_ro' diff --git a/networking_ccloud/common/exceptions.py b/networking_ccloud/common/exceptions.py index 0a36e8be..a1834308 100644 --- a/networking_ccloud/common/exceptions.py +++ b/networking_ccloud/common/exceptions.py @@ -51,3 +51,11 @@ class SwitchConnectionError(Exception): class SubnetSubnetPoolAZAffinityError(n_exc.BadRequest): message = ("The subnet's network %(network_id)s has AZ hint %(net_az_hint)s, " "the subnet's subnetpool %(subnetpool_id)s has AZ %(subnetpool_az)s set, which do not match") + + +class GenericTrunkException(n_exc.NeutronException): + message = "%(msg)s" + + +class BadTrunkRequest(n_exc.BadRequest): + message = "Bad request for %(trunk_port_id)s: %(reason)s" diff --git a/networking_ccloud/db/db_plugin.py b/networking_ccloud/db/db_plugin.py index ef0f290b..b40eb9ae 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,36 @@ 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 + + @db_api.retry_if_session_inactive() + def get_trunks_with_binding_host(self, context, host): + fields = [ + trunk_models.Trunk.id, + trunk_models.Trunk.port_id, + ml2_models.PortBinding.host, + ml2_models.PortBinding.profile, + ] + query = context.session.query(*fields) + query = query.join(ml2_models.PortBinding, + trunk_models.Trunk.port_id == ml2_models.PortBinding.port_id) + query = query.filter(sa.or_(ml2_models.PortBinding.host == host, + ml2_models.PortBinding.profile.like(f"%{host}%"))) + + trunk_ids = [] + for trunk_id, port_id, port_host, port_profile in query.all(): + port_profile_host = helper.get_binding_host_from_profile(port_profile, port_id) + if port_profile_host: + port_host = port_profile_host + if port_host != host: + continue + trunk_ids.append(trunk_id) + return trunk_ids 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/services/__init__.py b/networking_ccloud/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/networking_ccloud/services/trunk/__init__.py b/networking_ccloud/services/trunk/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/networking_ccloud/services/trunk/driver.py b/networking_ccloud/services/trunk/driver.py new file mode 100644 index 00000000..25191f91 --- /dev/null +++ b/networking_ccloud/services/trunk/driver.py @@ -0,0 +1,228 @@ +# Copyright 2023 SAP SE +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from neutron.services.trunk.drivers import base +from neutron_lib.api.definitions import port as p_api +from neutron_lib.api.definitions import portbindings as pb_api +from neutron_lib.callbacks import events +from neutron_lib.callbacks import registry +from neutron_lib.callbacks import resources +from neutron_lib import constants as nl_const +from neutron_lib.plugins import directory +from neutron_lib.services.trunk import constants as trunk_const +from oslo_config import cfg +from oslo_log import log as logging + +from networking_ccloud.common.config import get_driver_config +from networking_ccloud.common import constants as cc_const +from networking_ccloud.common.exceptions import BadTrunkRequest +from networking_ccloud.common import helper +from networking_ccloud.ml2.plugin import FabricPlugin + +LOG = logging.getLogger(__name__) + +SUPPORTED_INTERFACES = ( + cc_const.VIF_TYPE_CC_FABRIC, +) +SUPPORTED_SEGMENTATION_TYPES = ( + trunk_const.SEGMENTATION_TYPE_VLAN, +) + + +class CCTrunkDriver(base.DriverBase): + @property + def is_loaded(self): + try: + return cc_const.CC_DRIVER_NAME in cfg.CONF.ml2.mechanism_drivers + except cfg.NoSuchOptError: + return False + + @classmethod + def create(cls): + return cls(cc_const.CC_DRIVER_NAME, SUPPORTED_INTERFACES, SUPPORTED_SEGMENTATION_TYPES, + can_trunk_bound_port=True) + + def _get_parent_port(self, context, parent_port_id): + """Get parent port while checking it is compatible to our trunk driver + + Return None if this driver is not responsible for this trunk/port + """ + port = self.core_plugin.get_port(context, parent_port_id) + + # FIXME: normally we also should be able to work with unbound trunks + if not self.is_interface_compatible(port[pb_api.VIF_TYPE]): + LOG.debug("Parent port %s vif type %s not compatible", parent_port_id, port[pb_api.VIF_TYPE]) + return None + return port + + @registry.receives(resources.TRUNK_PLUGIN, [events.AFTER_INIT]) + def register(self, resource, event, trigger, payload=None): + super().register(resource, event, trigger, payload=payload) + + self.core_plugin = directory.get_plugin() + self.drv_conf = get_driver_config() + self.fabric_plugin = FabricPlugin() + + registry.subscribe(self.trunk_valid_precommit, resources.TRUNK, events.PRECOMMIT_CREATE) + registry.subscribe(self.trunk_create, resources.TRUNK, events.AFTER_CREATE) + registry.subscribe(self.trunk_delete, resources.TRUNK, events.AFTER_DELETE) + + registry.subscribe(self.subport_valid_precommit, resources.SUBPORTS, events.PRECOMMIT_CREATE) + registry.subscribe(self.subport_create, resources.SUBPORTS, events.AFTER_CREATE) + registry.subscribe(self.subport_delete, resources.SUBPORTS, events.AFTER_DELETE) + + def trunk_valid_precommit(self, resource, event, trunk_plugin, payload): + self.validate_trunk(payload.context, payload.desired_state, payload.desired_state.sub_ports) + + def subport_valid_precommit(self, resource, event, trunk_plugin, payload): + self.validate_trunk(payload.context, payload.states[0], payload.metadata['subports']) + + def validate_trunk(self, context, trunk, subports): + trunk_port = self._get_parent_port(context, trunk.port_id) + if not trunk_port: + LOG.debug("Not responsible for trunk on port %s", trunk.port_id) + return + + # we can only trunk direct bindings + trunk_host = helper.get_binding_host_from_port(trunk_port) + LOG.info("Validating trunk for trunk %s port %s host %s", trunk.id, trunk.port_id, trunk_host) + hg_config = self.drv_conf.get_hostgroup_by_host(trunk_host) + if not hg_config: + raise BadTrunkRequest(trunk_port_id=trunk.port_id, + reason=f"No hostgroup config found for host {trunk_host}") + + if not hg_config.direct_binding: + raise BadTrunkRequest(trunk_port_id=trunk.port_id, + reason=f"Hostgroup {trunk_host} is not a direct binding hostgroup " + "(maybe a metagroup?), only direct binding hostgroups can be trunked") + + if hg_config.role is not None: + raise BadTrunkRequest(trunk_port_id=trunk.port_id, + reason=f"Hostgroup {trunk_host} is of role {hg_config.role} " + "and can therefore not be trunked") + + trunks_on_host = self.fabric_plugin.get_trunks_with_binding_host(context, trunk_host) + trunks = set(trunks_on_host) - set([trunk.id]) + if trunks: + raise BadTrunkRequest(trunk_port_id=trunk.port_id, + reason=f"Host {trunk_host} already has trunk {' '.join(trunks)} connected to it") + + # subport validation + parent_hg = hg_config.get_parent_metagroup(self.drv_conf) + meta_hg_vlans = [] + if parent_hg: + meta_hg_vlans = hg_config.get_any_switchgroup(self.drv_conf).get_managed_vlans(self.drv_conf, + with_infra_nets=True) + + subport_nets = {} + # existing subports + for existing_subport in trunk.sub_ports: + subport_port = self.core_plugin.get_port(context, existing_subport.port_id) + subport_nets[subport_port['network_id']] = existing_subport.port_id + + # new subports + for subport in subports: + # don't allow a network to be on two subports + subport_port = self.core_plugin.get_port(context, subport.port_id) + sp_net = subport_port['network_id'] + if sp_net in subport_nets and subport_nets[sp_net] != subport.port_id: + raise BadTrunkRequest(trunk_port_id=trunk.port_id, + reason=f"Network {sp_net} cannot be on two subports, " + f"{subport_nets[sp_net]} and port {subport.port_id}") + subport_nets[sp_net] = subport.port_id + + # for hostgroups that are in a metagroup we don't want to trunk anything that trunks toward a vlan id + # that might be used by the metagroup + if subport.segmentation_id in meta_hg_vlans: + sg_name = hg_config.get_any_switchgroup(self.drv_conf).name + raise BadTrunkRequest(trunk_port_id=trunk.port_id, + reason=f"Subport {subport.port_id} segmentation id {subport.segmentation_id} " + f"collides with vlan range of switchgroup {sg_name}") + + def trunk_create(self, resource, event, trunk_plugin, payload): + trunk_port = self._get_parent_port(payload.context, payload.states[0].port_id) + if not trunk_port: + return + self._bind_subports(payload.context, trunk_port, payload.states[0], payload.states[0].sub_ports) + status = trunk_const.TRUNK_ACTIVE_STATUS if len(payload.states[0].sub_ports) else trunk_const.TRUNK_DOWN_STATUS + payload.states[0].update(status=status) + + def trunk_delete(self, resource, event, trunk_plugin, payload): + trunk_port = self._get_parent_port(payload.context, payload.states[0].port_id) + if not trunk_port: + return + self._unbind_subports(payload.context, trunk_port, payload.states[0], payload.states[0].sub_ports) + + def subport_create(self, resource, event, trunk_plugin, payload): + trunk_port = self._get_parent_port(payload.context, payload.states[0].port_id) + if not trunk_port: + return + self._bind_subports(payload.context, trunk_port, payload.states[0], payload.metadata['subports']) + + def subport_delete(self, resource, event, trunk_plugin, payload): + trunk_port = self._get_parent_port(payload.context, payload.states[0].port_id) + if not trunk_port: + return + self._unbind_subports(payload.context, trunk_port, payload.states[0], payload.metadata['subports']) + + def _bind_subports(self, context, trunk_port, trunk, subports): + for subport in subports: + LOG.info("Adding subport %s trunk port %s of trunk %s", subport.port_id, trunk.port_id, trunk.id) + binding_profile = trunk_port.get(pb_api.PROFILE) + + # note, that this information is only informational + binding_profile[cc_const.TRUNK_PROFILE] = { + 'segmentation_type': subport.segmentation_type, + 'segmentation_id': subport.segmentation_id, + 'trunk_id': trunk.id, + } + + port_data = { + p_api.RESOURCE_NAME: { + pb_api.HOST_ID: trunk_port.get(pb_api.HOST_ID), + pb_api.VNIC_TYPE: trunk_port.get(pb_api.VNIC_TYPE), + pb_api.PROFILE: binding_profile, + 'device_owner': trunk_const.TRUNK_SUBPORT_OWNER, + 'device_id': trunk_port.get('device_id'), + } + } + self.core_plugin.update_port(context, subport.port_id, port_data) + if len(subports) > 0: + trunk.update(status=trunk_const.TRUNK_ACTIVE_STATUS) + + def _unbind_subports(self, context, trunk_port, trunk, subports): + for subport in subports: + LOG.info("Removing subport %s trunk port %s of trunk %s", subport.port_id, trunk.port_id, trunk.id) + binding_profile = trunk_port.get(pb_api.PROFILE) + + # note, that this is only informational + if cc_const.TRUNK_PROFILE in binding_profile: + del binding_profile[cc_const.TRUNK_PROFILE] + + port_data = { + p_api.RESOURCE_NAME: { + pb_api.HOST_ID: None, + pb_api.VNIC_TYPE: None, + pb_api.PROFILE: binding_profile, + 'device_owner': '', + 'device_id': '', + 'status': nl_const.PORT_STATUS_DOWN, + }, + } + self.core_plugin.update_port(context, subport.port_id, port_data) + + if len(trunk.sub_ports) - len(subports) > 0: + trunk.update(status=trunk_const.TRUNK_ACTIVE_STATUS) + else: + LOG.info("Last subport was removed from trunk %s, setting it to state DOWN", trunk.id) 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..261675cd 100644 --- a/networking_ccloud/tests/unit/db/test_db_plugin.py +++ b/networking_ccloud/tests/unit/db/test_db_plugin.py @@ -12,11 +12,14 @@ # License for the specific language governing permissions and limitations # under the License. +import json + from neutron.db.models import address_scope as ascope_models from neutron.db.models import external_net as extnet_models from neutron.db.models import segment as segment_models from neutron.db.models import tag as tag_models from neutron.db import models_v2 +from neutron.plugins.ml2 import models as ml2_models from neutron.services.tag import tag_plugin from neutron.services.trunk import models as trunk_models from neutron.tests.unit.extensions import test_segment @@ -304,6 +307,39 @@ 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'])) + + def test_get_trunks_with_binding_host(self): + ctx = context.get_admin_context() + with self.port() as trunkport1, self.port() as trunkport2: + with ctx.session.begin(): + trunk1 = trunk_models.Trunk(name='random-trunk1', port_id=trunkport1['port']['id'], sub_ports=[]) + binding1 = (ctx.session.query(ml2_models.PortBinding) + .filter(ml2_models.PortBinding.port_id == trunkport1['port']['id']).first()) + binding1.host = 'seagull' + ctx.session.add(trunk1, binding1) + trunk2 = trunk_models.Trunk(name='random-trunk2', port_id=trunkport2['port']['id'], sub_ports=[]) + binding2 = (ctx.session.query(ml2_models.PortBinding) + .filter(ml2_models.PortBinding.port_id == trunkport2['port']['id']).first()) + binding2.profile = json.dumps({"local_link_information": [{"switch_info": "oystercatcher"}]}) + ctx.session.add(trunk2, binding2) + + self.assertEqual([trunk1.id], self._db.get_trunks_with_binding_host(ctx, "seagull")) + self.assertEqual([trunk2.id], self._db.get_trunks_with_binding_host(ctx, "oystercatcher")) + self.assertEqual([], self._db.get_trunks_with_binding_host(ctx, "whatever")) + 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..bfb9eafa 100644 --- a/networking_ccloud/tests/unit/ml2/test_mech_driver.py +++ b/networking_ccloud/tests/unit/ml2/test_mech_driver.py @@ -23,6 +23,7 @@ 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.tests.common import helpers as neutron_test_helpers from neutron.tests.unit.plugins.ml2 import test_plugin @@ -33,6 +34,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 +57,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 +177,64 @@ 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(): + 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: