From 0a952ce78638cc34b57a4126fd1843d80596bdf6 Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Mon, 11 Mar 2019 03:50:49 -0700 Subject: [PATCH] [NSX-v3] Use bridge endpoint profiles for L2 gateways Replace NSX bridge cluster with bridge endpoint profiles as the backend resource used to implement L2 gateways. The logic for creating a gateway connection is not changed, with the only exception that bridge endpoints now have a reference to a bridge endpoint profile. Connections created using bridge clusters can be safely removed, while creation of new connection on gateways leveraging bridge clusters will fail. Change-Id: I29cd9a2501ab4b7dd226729f33ab962bbba2dfff --- vmware_nsx/common/config.py | 8 + vmware_nsx/common/exceptions.py | 5 + vmware_nsx/db/db.py | 5 - vmware_nsx/plugins/nsx_v3/plugin.py | 3 + .../services/l2gateway/nsx_v3/driver.py | 358 ++++++++++++------ vmware_nsx/tests/unit/nsx_v3/test_plugin.py | 5 - .../services/l2gateway/test_nsxv3_driver.py | 158 +++++--- 7 files changed, 360 insertions(+), 182 deletions(-) diff --git a/vmware_nsx/common/config.py b/vmware_nsx/common/config.py index ff43fdd662..bbfa721ec2 100644 --- a/vmware_nsx/common/config.py +++ b/vmware_nsx/common/config.py @@ -417,6 +417,7 @@ nsx_v3_opts = nsx_v3_and_p + [ "Neutron networks, if no physical network has been " "specified")), cfg.StrOpt('default_bridge_cluster', + deprecated_for_removal=True, help=_("(Optional) Name or UUID of the default NSX bridge " "cluster that will be used to perform L2 gateway " "bridging between VXLAN and VLAN networks. If default " @@ -425,6 +426,13 @@ nsx_v3_opts = nsx_v3_and_p + [ "NSX Bridge Cluster using L2 gateway APIs. This field " "must be specified on one of the active neutron " "servers only.")), + cfg.StrOpt('default_bridge_endpoint_profile', + help=_("(Optional) Name or UUID of the default NSX bridge " + "endpoint profile that will be used to perform L2 " + "bridging between networks in the NSX fabric and " + "VLANs external to NSX. If not specified, operators " + "will need to explictly create a layer-2 gateway in " + "Neutron using the L2 gateway APIs.")), cfg.StrOpt('default_tier0_router', help=_("Name or UUID of the default tier0 router that will be " "used for connecting to tier1 logical routers and " diff --git a/vmware_nsx/common/exceptions.py b/vmware_nsx/common/exceptions.py index 2eb2364422..9c8ee94faa 100644 --- a/vmware_nsx/common/exceptions.py +++ b/vmware_nsx/common/exceptions.py @@ -76,6 +76,11 @@ class L2GatewayAlreadyInUse(n_exc.Conflict): message = _("Gateway Service %(gateway)s is already in use") +class BridgeEndpointAttachmentInUse(n_exc.Conflict): + message = _("The NSX backend only allow a single L2 gateway connection " + "for network %(network_id)s") + + class InvalidTransportType(NsxPluginException): message = _("The transport type %(transport_type)s is not recognized " "by the backend") diff --git a/vmware_nsx/db/db.py b/vmware_nsx/db/db.py index e7e421be79..73aa785bb8 100644 --- a/vmware_nsx/db/db.py +++ b/vmware_nsx/db/db.py @@ -391,11 +391,6 @@ def get_l2gw_connection_mapping(session, connection_id): pass -def get_l2gw_connection_mappings_by_bridge(session, bridge_endpoint_id): - return (session.query(nsx_models.NsxL2GWConnectionMapping). - filter_by(bridge_endpoint_id=bridge_endpoint_id).all()) - - # NSXv3 QoS policy id <-> switch Id mapping def add_qos_policy_profile_mapping(session, qos_policy_id, switch_profile_id): with session.begin(subtransactions=True): diff --git a/vmware_nsx/plugins/nsx_v3/plugin.py b/vmware_nsx/plugins/nsx_v3/plugin.py index d5e9e1f8d3..42db53d45a 100644 --- a/vmware_nsx/plugins/nsx_v3/plugin.py +++ b/vmware_nsx/plugins/nsx_v3/plugin.py @@ -1409,6 +1409,9 @@ class NsxV3Plugin(nsx_plugin_common.NsxPluginV3Base, # (for example - transport zone with KVM) LOG.exception("Unable to create port on the backend: %s", inst) + if inst.error_code == 8407: + raise nsx_exc.BridgeEndpointAttachmentInUse( + network_id=port_data['network_id']) msg = _("Unable to create port on the backend") raise nsx_exc.NsxPluginException(err_msg=msg) diff --git a/vmware_nsx/services/l2gateway/nsx_v3/driver.py b/vmware_nsx/services/l2gateway/nsx_v3/driver.py index cde4684426..67d1a35051 100644 --- a/vmware_nsx/services/l2gateway/nsx_v3/driver.py +++ b/vmware_nsx/services/l2gateway/nsx_v3/driver.py @@ -40,18 +40,22 @@ from vmware_nsx.db import db as nsx_db from vmware_nsx.extensions import projectpluginmap from vmware_nsxlib.v3 import exceptions as nsxlib_exc from vmware_nsxlib.v3 import nsx_constants +from vmware_nsxlib.v3 import utils as nsxlib_utils + LOG = logging.getLogger(__name__) +class _NotUniqueL2GW(Exception): + """Raised if validation of default L2 GW uniqueness fails.""" + + class NsxV3Driver(l2gateway_db.L2GatewayMixin): """Class to handle API calls for L2 gateway and NSXv3 backend.""" gateway_resource = l2gw_const.GATEWAY_RESOURCE_NAME def __init__(self, plugin): - # Create a default L2 gateway if default_bridge_cluster is - # provided in nsx.ini super(NsxV3Driver, self).__init__() self._plugin = plugin LOG.debug("Starting service plugin for NSX L2Gateway") @@ -75,62 +79,142 @@ class NsxV3Driver(l2gateway_db.L2GatewayMixin): registry.subscribe(self._ensure_default_l2_gateway, resources.PROCESS, events.BEFORE_SPAWN) + def _find_default_l2_gateway(self, admin_ctx, def_device_id): + for l2gateway in self._get_l2_gateways(admin_ctx): + if l2gateway['devices'][0]['device_name'] == def_device_id: + return l2gateway + + @nsxlib_utils.retry_random_upon_exception(_NotUniqueL2GW, max_attempts=10) + def _create_default_l2_gateway(self, admin_ctx, l2gw_dict, def_device_id): + LOG.debug("Creating default layer-2 gateway with: %s", l2gw_dict) + def_l2gw = super(NsxV3Driver, self).create_l2_gateway(admin_ctx, + l2gw_dict) + # Verify that no other instance of neutron-server had the same + # brilliant idea... + l2gateways = self._get_l2_gateways(admin_ctx) + for l2gateway in l2gateways: + # Since we ensure L2 gateway is created with only 1 device, we use + # the first device in the list. + if l2gateway['devices'][0]['device_name'] == def_device_id: + if l2gateway['id'] == def_l2gw['id']: + # Nothing to worry about, that's our gateway + continue + LOG.info("Default L2 gateway is already created with " + "id %s, deleting L2 gateway with id %s", + l2gateway['id'], def_l2gw['id']) + # Commit suicide! + self.validate_l2_gateway_for_delete( + admin_ctx, def_l2gw) + # We can be sure the gateway is not in use... + super(NsxV3Driver, self).delete_l2_gateway( + admin_ctx, def_l2gw['id']) + # The operation should be retried to avoid the situation where + # every instance deletes the gateway it created + raise _NotUniqueL2GW + + return def_l2gw + + def _get_bridge_vlan_tz_id(self, bep_data): + nsxlib = self._core_plugin.nsxlib + # Edge cluster Id is mandatory, do not fear KeyError + edge_cluster_id = bep_data['edge_cluster_id'] + member_indexes = bep_data.get('edge_cluster_member_indexes', []) + # NSX should not allow bridge endpoint profiles attached to + # non-existing edge clusters + edge_cluster = nsxlib.edge_cluster.get(edge_cluster_id) + member_map = dict((member['member_index'], + member['transport_node_id']) + for member in edge_cluster['members']) + # By default consider all transport nodes in the cluster for + # retrieving the VLAN transprtzone + tn_ids = member_map.values() + if member_indexes: + try: + tn_ids = [member_map[idx] for idx in member_indexes] + except KeyError: + LOG.warning("Invalid member indexes specified in bridge " + "endpoint profile: %(bep_id)s: %(indexes)s", + {'bep_id': bep_data['id'], + 'indexes': member_indexes}) + + # Retrieve VLAN transport zones + vlan_transport_zones = nsxlib.search_all_resource_by_attributes( + nsxlib.transport_zone.resource_type, + transport_type='VLAN') + vlan_tz_map = dict((vlan_tz['id'], []) + for vlan_tz in vlan_transport_zones) + for tn_id in tn_ids: + tn_data = nsxlib.transport_node.get(tn_id) + for tz_endpoint in tn_data.get('transport_zone_endpoints', []): + tz_id = tz_endpoint['transport_zone_id'] + if tz_id in vlan_tz_map: + vlan_tz_map[tz_id].append(tn_id) + + # Find the VLAN transport zone that is used by all transport nodes + results = [] + for (tz_id, nodes) in vlan_tz_map.items(): + if set(nodes) != set(tn_ids): + continue + results.append(tz_id) + + return results + def _ensure_default_l2_gateway(self, resource, event, trigger, payload=None): """ Create a default logical L2 gateway. - Create a logical L2 gateway in the neutron database if the - default_bridge_cluster config parameter is set and if it is - not previously created. If not set, return. + Create a logical L2 gateway in the neutron database for the + default bridge endpoint profile, if specified in the configuration. + Ensure only one gateway is configured in presence of multiple + neutron servers. """ - def_l2gw_name = cfg.CONF.nsx_v3.default_bridge_cluster - # Return if no default_bridge_cluster set in config - if not def_l2gw_name: - LOG.info("NSX: Default bridge cluster not configured " - "in nsx.ini. No default L2 gateway created.") + if cfg.CONF.nsx_v3.default_bridge_cluster: + LOG.warning("Attention! The default_bridge_cluster option is " + "still set to %s. This option won't have any effect " + "as L2 gateways based on bridge clusters are not " + "implemented anymore", + cfg.CONF.nsx_v3.default_bridge_cluster) + def_bep = cfg.CONF.nsx_v3.default_bridge_endpoint_profile + # Return if no default_endpoint_profile set in config + if not def_bep: + LOG.info("NSX Default bridge endpoint profile not set. " + "Default L2 gateway will not be processed.") return admin_ctx = context.get_admin_context() - - def_l2gw_uuid = ( - self._core_plugin.nsxlib.bridge_cluster.get_id_by_name_or_id( - def_l2gw_name)) - - # Optimistically create the default L2 gateway in neutron DB - device = {'device_name': def_l2gw_uuid, - 'interfaces': [{'name': 'default-bridge-cluster'}]} + nsx_bep_client = self._core_plugin.nsxlib.bridge_endpoint_profile + bep_id = nsx_bep_client.get_id_by_name_or_id(def_bep) + def_l2gw = self._find_default_l2_gateway(admin_ctx, bep_id) + # If there is already an existing gateway, use that one + if def_l2gw: + LOG.info("A default L2 gateway for bridge endpoint profile " + "%(bep_id)s already exists. Reusing L2 gateway " + "%(def_l2gw_id)s)", + {'bep_id': bep_id, 'def_l2gw_id': def_l2gw['id']}) + return def_l2gw + bep_data = nsx_bep_client.get(bep_id) + vlan_tzs = self._get_bridge_vlan_tz_id(bep_data) + if not vlan_tzs: + LOG.info("No NSX VLAN transport zone could be used for bridge " + "endpoint profile: %s. Default L2 gateway will not " + "be processed", bep_id) + return + # TODO(salv-orlando): Implement support for multiple VLAN TZ + vlan_tz = vlan_tzs[0] + if len(vlan_tzs) > 1: + LOG.info("The NSX L2 gateway driver currenly supports a single " + "VLAN transport zone for bridging, but %(num_tz)d " + "were specified. Transport zone %(tz)s will be used " + "for L2 gateways", + {'num_tz': len(vlan_tzs), 'tz': vlan_tz}) + device = {'device_name': bep_id, + 'interfaces': [{'name': vlan_tz}]} # TODO(asarfaty): Add a default v3 tenant-id to allow TVD filtering - def_l2gw = {'name': 'default-l2gw', - 'devices': [device]} - l2gw_dict = {self.gateway_resource: def_l2gw} - self.create_l2_gateway(admin_ctx, l2gw_dict) - l2_gateway = super(NsxV3Driver, self).create_l2_gateway(admin_ctx, - l2gw_dict) - # Verify that only one default L2 gateway is created - def_l2gw_exists = False - l2gateways = self._get_l2_gateways(admin_ctx) - for l2gateway in l2gateways: - # Since we ensure L2 gateway is created with only 1 device, we use - # the first device in the list. - if l2gateway['devices'][0]['device_name'] == def_l2gw_uuid: - if def_l2gw_exists: - LOG.info("Default L2 gateway is already created.") - try: - # Try deleting this duplicate default L2 gateway - self.validate_l2_gateway_for_delete( - admin_ctx, l2gateway['id']) - super(NsxV3Driver, self).delete_l2_gateway( - admin_ctx, l2gateway['id']) - except l2gw_exc.L2GatewayInUse: - # If the L2 gateway we are trying to delete is in - # use then we should delete the L2 gateway which - # we just created ensuring there is only one - # default L2 gateway in the database. - super(NsxV3Driver, self).delete_l2_gateway( - admin_ctx, l2_gateway['id']) - else: - def_l2gw_exists = True - return l2_gateway + l2gw_dict = {self.gateway_resource: { + 'name': 'default-nsxedge-l2gw', + 'devices': [device]}} + self._create_default_l2_gateway(admin_ctx, l2gw_dict, bep_id) + return def_l2gw def _prevent_l2gw_port_delete(self, resource, event, trigger, payload=None): @@ -140,30 +224,34 @@ class NsxV3Driver(l2gateway_db.L2GatewayMixin): if port_check: self.prevent_l2gw_port_deletion(context, port_id) - def _validate_device_list(self, devices): - # In NSXv3, one L2 gateway is mapped to one bridge cluster. + def _validate_device_list(self, devices, check_backend=True): + # In NSXv3, one L2 gateway is mapped to one bridge endpoint profle. # So we expect only one device to be configured as part of # a L2 gateway resource. The name of the device must be the bridge - # cluster's UUID. + # endpoint profile UUID. if len(devices) != 1: - msg = _("Only a single device is supported for one L2 gateway") + msg = _("Only a single device is supported by the NSX L2" + "gateway driver") raise n_exc.InvalidInput(error_message=msg) - if not uuidutils.is_uuid_like(devices[0]['device_name']): + dev_name = devices[0]['device_name'] + if not uuidutils.is_uuid_like(dev_name): msg = _("Device name must be configured with a UUID") raise n_exc.InvalidInput(error_message=msg) - # Make sure the L2GW device ID exists as Bridge Cluster on NSX. - try: - self._core_plugin.nsxlib.bridge_cluster.get( - devices[0]['device_name']) - except nsxlib_exc.ResourceNotFound: - msg = _("Could not find Bridge Cluster for L2 gateway device " - "%s on NSX backend") % devices[0]['device_name'] - LOG.error(msg) - raise n_exc.InvalidInput(error_message=msg) + # Ensure the L2GW device is a valid bridge endpoint profile in NSX + if check_backend: + try: + self._core_plugin.nsxlib.bridge_endpoint_profile.get( + dev_name) + except nsxlib_exc.ResourceNotFound: + msg = _("Could not find Bridge Endpoint Profile for L2 " + "gateway device %s on NSX backend") % dev_name + LOG.error(msg) + raise n_exc.InvalidInput(error_message=msg) # One L2 gateway must have only one interface defined. interfaces = devices[0].get(l2gw_const.IFACE_NAME_ATTR) if len(interfaces) > 1: - msg = _("Maximum of one interface is supported for one L2 gateway") + msg = _("Maximum of one interface is supported by the NSX L2 " + "gateway driver") raise n_exc.InvalidInput(error_message=msg) def create_l2_gateway(self, context, l2_gateway): @@ -196,10 +284,9 @@ class NsxV3Driver(l2gateway_db.L2GatewayMixin): def _validate_network(self, context, network_id): network = self._core_plugin.get_network(context, network_id) network_type = network.get(providernet.NETWORK_TYPE) - # If network is a provider network, verify whether it is of type VXLAN - if network_type and network_type != nsx_utils.NsxV3NetworkTypes.VXLAN: - msg = (_("Unsupported network type %s for L2 gateway " - "connection. Only VXLAN network type supported") % + # If network is a provider network, verify whether it is of type GENEVE + if network_type and network_type != nsx_utils.NsxV3NetworkTypes.GENEVE: + msg = (_("Unsupported network type %s for L2 gateway connection") % network_type) raise n_exc.InvalidInput(error_message=msg) @@ -213,33 +300,67 @@ class NsxV3Driver(l2gateway_db.L2GatewayMixin): network_id = gw_connection.get(l2gw_const.NETWORK_ID) self._validate_network(context, network_id) - def _get_bridge_cluster(self, context, l2gw_id): + def _get_bep(self, context, l2gw_id): # In NSXv3, there will be only one device configured per L2 gateway. - # The name of the device shall carry the backend bridge cluster's UUID. + # The name of the device shall carry the bridge endpoint profile id. devices = self._get_l2_gateway_devices(context, l2gw_id) return devices[0].get('device_name') - def _get_conn_seg_id(self, context, gw_connection): + def _get_conn_parameters(self, context, gw_connection): + """Return interface and segmenantion id for a connection. """ if not gw_connection: return + l2gw_id = gw_connection.get(l2gw_const.L2GATEWAY_ID) seg_id = gw_connection.get(l2gw_const.SEG_ID) + devices = self._get_l2_gateway_devices(context, l2gw_id) + # TODO(salv-orlando): support more than a single interface + interface = self._get_l2_gw_interfaces(context, devices[0]['id'])[0] if not seg_id: # Seg-id was not passed as part of connection-create. Retrieve # seg-id from L2 gateway's interface. - l2gw_id = gw_connection.get(l2gw_const.L2GATEWAY_ID) - devices = self._get_l2_gateway_devices(context, l2gw_id) - interface = self._get_l2_gw_interfaces(context, devices[0]['id']) - seg_id = interface[0].get(l2gw_const.SEG_ID) - return seg_id + seg_id = interface.get('segmentation_id') + return interface['interface_name'], seg_id def create_l2_gateway_connection_precommit(self, context, gw_connection): """Validate the L2 gateway connection Do not allow another connection with the same bride cluster and seg_id """ admin_ctx = context.elevated() + nsxlib = self._core_plugin.nsxlib l2gw_id = gw_connection.get(l2gw_const.L2GATEWAY_ID) - seg_id = self._get_conn_seg_id(admin_ctx, gw_connection) - bridge_cluster = self._get_bridge_cluster(admin_ctx, l2gw_id) + devices = self._get_l2_gateway_devices(context, l2gw_id) + bep_id = devices[0].get('device_name') + # Check for bridge endpoint profile existence + # if bridge endpoint profile is not found, this is likely an old + # connection, fail with error. + try: + nsxlib.bridge_endpoint_profile.get_id_by_name_or_id(bep_id) + except nsxlib_exc.ManagerError as e: + msg = (_("Error while retrieving bridge endpoint profile " + "%(bep_id)s from NSX backend. Check that the profile " + "exits and there are not multiple profiles with " + "the given name. Exception: %(exc)s") % + {'bep_id': bep_id, 'exc': e}) + raise n_exc.InvalidInput(error_message=msg) + + interface_name, seg_id = self._get_conn_parameters( + admin_ctx, gw_connection) + try: + # Use search API for listing bridge endpoints on NSX for provided + # VLAN id, transport zone id, and Bridge endpoint profile + endpoints = nsxlib.search_all_resource_by_attributes( + nsxlib.bridge_endpoint.resource_type, + bridge_endpoint_profile_id=bep_id, + vlan_transport_zone_id=interface_name, + vlan=seg_id) + endpoint_map = dict((endpoint['id'], + endpoint['bridge_endpoint_profile_id']) + for endpoint in endpoints) + except nsxlib_exc.ManagerError as e: + msg = (_("Error while retrieving endpoints for bridge endpoint " + "profile %(bep_id)s s from NSX backend. " + "Exception: %(exc)s") % {'bep_id': bep_id, 'exc': e}) + raise n_exc.InvalidInput(error_message=msg) # get all bridge endpoint ports with db_api.CONTEXT_WRITER.using(admin_ctx): @@ -247,65 +368,55 @@ class NsxV3Driver(l2gateway_db.L2GatewayMixin): ports = self._core_plugin.get_ports( admin_ctx, filters=port_filters) for port in ports: - # get the nsx mapping by bridge endpoint - if port.get('device_id'): - mappings = nsx_db.get_l2gw_connection_mappings_by_bridge( - admin_ctx.session, port['device_id']) - for mapping in mappings: - conn_id = mapping.connection_id - # get the matching GW connection - conn = self._get_l2_gateway_connection( - admin_ctx, conn_id) - con_seg_id = self._get_conn_seg_id(admin_ctx, conn) - if (conn and con_seg_id and - int(con_seg_id) == int(seg_id)): - # compare the bridge cluster - conn_bridge_cluster = self._get_bridge_cluster( - admin_ctx, conn.l2_gateway_id) - if conn_bridge_cluster == bridge_cluster: - msg = (_("Cannot create multiple connections " - "with the same segmentation id " - "%(seg_id)s for bridge cluster " - "%(bridge)s") % { - 'seg_id': seg_id, - 'bridge': bridge_cluster}) - raise n_exc.InvalidInput(error_message=msg) + device_id = port.get('device_id') + if endpoint_map.get(device_id) == bep_id: + # This device is using the same vlan id and bridge endpoint + # profile as the one requested. Not ok. + msg = (_("Cannot create multiple connections with the " + "same segmentation id %(seg_id)s for bridge " + "endpoint profile %(bep_id)s") % + {'seg_id': seg_id, + 'bep_id': bep_id}) + raise n_exc.InvalidInput(error_message=msg) def create_l2_gateway_connection_postcommit(self, context, gw_connection): - """Create a L2 gateway connection.""" + """Create a L2 gateway connection on the backend""" + nsxlib = self._core_plugin.nsxlib l2gw_id = gw_connection.get(l2gw_const.L2GATEWAY_ID) network_id = gw_connection.get(l2gw_const.NETWORK_ID) - device_name = self._get_bridge_cluster(context, l2gw_id) - seg_id = self._get_conn_seg_id(context, gw_connection) + device_name = self._get_bep(context, l2gw_id) + interface_name, seg_id = self._get_conn_parameters( + context, gw_connection) self._validate_segment_id(seg_id) tenant_id = gw_connection['tenant_id'] if context.is_admin and not tenant_id: tenant_id = context.tenant_id gw_connection['tenant_id'] = tenant_id try: - tags = self._core_plugin.nsxlib.build_v3_tags_payload( + tags = nsxlib.build_v3_tags_payload( gw_connection, resource_type='os-neutron-l2gw-id', project_name=context.tenant_name) - bridge_endpoint = self._core_plugin.nsxlib.bridge_endpoint.create( + bridge_endpoint = nsxlib.bridge_endpoint.create( device_name=device_name, - seg_id=seg_id, + vlan_transport_zone_id=interface_name, + vlan_id=seg_id, tags=tags) except nsxlib_exc.ManagerError as e: - LOG.exception("Unable to create bridge endpoint, rolling back " - "changes on neutron. Exception is %s", e) + LOG.exception("Unable to create bridge endpoint. " + "Exception is %s", e) raise l2gw_exc.L2GatewayServiceDriverError( method='create_l2_gateway_connection_postcommit') - #TODO(abhiraut): Consider specifying the name of the port - # Create a logical port and connect it to the bridge endpoint. + port_dict = {'port': { + 'name': 'l2gw-conn-%s-%s' % ( + l2gw_id, seg_id), 'tenant_id': tenant_id, 'network_id': network_id, 'mac_address': constants.ATTR_NOT_SPECIFIED, 'admin_state_up': True, 'fixed_ips': [], 'device_id': bridge_endpoint['id'], - 'device_owner': nsx_constants.BRIDGE_ENDPOINT, - 'name': '', }} + 'device_owner': nsx_constants.BRIDGE_ENDPOINT}} try: #TODO(abhiraut): Consider adding UT for port check once UTs are # refactored @@ -319,12 +430,16 @@ class NsxV3Driver(l2gateway_db.L2GatewayMixin): LOG.debug("IP addresses deallocated on port %s", port['id']) except (nsxlib_exc.ManagerError, n_exc.NeutronException) as e: - LOG.exception("Unable to create L2 gateway port, " - "rolling back changes on neutron: %s", e) - self._core_plugin.nsxlib.bridge_endpoint.delete( - bridge_endpoint['id']) - raise l2gw_exc.L2GatewayServiceDriverError( - method='create_l2_gateway_connection_postcommit') + with excutils.save_and_reraise_exception(): + LOG.exception("Unable to create L2 gateway port, " + "rolling back changes on backend: %s", e) + self._core_plugin.nsxlib.bridge_endpoint.delete( + bridge_endpoint['id']) + super(NsxV3Driver, + self).delete_l2_gateway_connection( + context, + gw_connection['id']) + try: # Update neutron's database with the mappings. nsx_db.add_l2gw_connection_mapping( @@ -335,7 +450,10 @@ class NsxV3Driver(l2gateway_db.L2GatewayMixin): except db_exc.DBError: with excutils.save_and_reraise_exception(): LOG.exception("Unable to add L2 gateway connection " - "mappings, rolling back changes on neutron") + "mappings for %(conn_id)s on network " + "%(net_id)s. rolling back changes.", + {'conn_id': gw_connection['id'], + 'net_id': network_id}) self._core_plugin.nsxlib.bridge_endpoint.delete( bridge_endpoint['id']) super(NsxV3Driver, diff --git a/vmware_nsx/tests/unit/nsx_v3/test_plugin.py b/vmware_nsx/tests/unit/nsx_v3/test_plugin.py index 63783ea10e..dd9dbe6eb8 100644 --- a/vmware_nsx/tests/unit/nsx_v3/test_plugin.py +++ b/vmware_nsx/tests/unit/nsx_v3/test_plugin.py @@ -121,11 +121,6 @@ def _mock_nsx_backend_calls(): "create_port_mirror_profile", side_effect=_return_id_key).start() - mock.patch( - "vmware_nsxlib.v3.core_resources.NsxLibBridgeCluster." - "get_id_by_name_or_id", - return_value=uuidutils.generate_uuid()).start() - mock.patch( "vmware_nsxlib.v3.core_resources.NsxLibBridgeEndpoint.create", side_effect=_return_id_key).start() diff --git a/vmware_nsx/tests/unit/services/l2gateway/test_nsxv3_driver.py b/vmware_nsx/tests/unit/services/l2gateway/test_nsxv3_driver.py index 855dcce226..a9c6c2eb22 100644 --- a/vmware_nsx/tests/unit/services/l2gateway/test_nsxv3_driver.py +++ b/vmware_nsx/tests/unit/services/l2gateway/test_nsxv3_driver.py @@ -25,21 +25,23 @@ from oslo_utils import importutils from oslo_utils import uuidutils from neutron.tests import base +from neutron_lib.api.definitions import provider_net as providernet from neutron_lib.callbacks import events from neutron_lib.callbacks import registry from neutron_lib.callbacks import resources from neutron_lib import context from neutron_lib import exceptions as n_exc +from vmware_nsx.common import utils as nsx_utils from vmware_nsx.services.l2gateway.nsx_v3 import driver as nsx_v3_driver from vmware_nsx.tests.unit.nsx_v3 import test_plugin as test_nsx_v3_plugin -from vmware_nsxlib.tests.unit.v3 import mocks as nsx_v3_mocks from vmware_nsxlib.v3 import nsx_constants NSX_V3_PLUGIN_CLASS = ('vmware_nsx.plugins.nsx_v3.plugin.NsxV3Plugin') NSX_V3_L2GW_DRIVER_CLASS_PATH = ('vmware_nsx.services.l2gateway.' 'nsx_v3.driver.NsxV3Driver') +NSX_DEFAULT_BEP_NAME = "default-bridge-endpoint-profile" class TestNsxV3L2GatewayDriver(test_l2gw_db.L2GWTestCase, @@ -58,12 +60,29 @@ class TestNsxV3L2GatewayDriver(test_l2gw_db.L2GWTestCase, mock.patch.object(l2gateway_db, 'subscribe') mock.patch('neutron.db.servicetype_db.ServiceTypeManager.get_instance', return_value=mock.MagicMock()).start() + mock_default_bep_uuid = uuidutils.generate_uuid() + mock.patch('vmware_nsxlib.v3.core_resources.' + 'NsxLibBridgeEndpointProfile.get_id_by_name_or_id', + return_value=mock_default_bep_uuid).start() + mock.patch('vmware_nsxlib.v3.core_resources.' + 'NsxLibBridgeEndpointProfile.get', + return_value={'id': mock_default_bep_uuid, + 'edge_cluster_id': 'meh'}).start() + mock.patch('vmware_nsxlib.v3.core_resources.' + 'NsxLibTransportZone.get_transport_type', + return_value="VLAN").start() + self.l2gw_plugin = core_l2gw_plugin.L2GatewayPlugin() self.context = context.get_admin_context() - def _get_nw_data(self): + def _get_nw_data(self, provider=False): net_data = super(TestNsxV3L2GatewayDriver, self)._get_nw_data() - net_data['network']['port_security_enabled'] = True + net_spec = net_data['network'] + net_spec['port_security_enabled'] = True + if provider: + net_spec[providernet.NETWORK_TYPE] = ( + nsx_utils.NsxV3NetworkTypes.VLAN) + net_spec[providernet.SEGMENTATION_ID] = 666 return net_data def test_nsxl2gw_driver_init(self): @@ -76,50 +95,55 @@ class TestNsxV3L2GatewayDriver(test_l2gw_db.L2GWTestCase, self.assertTrue(debug.called) def test_create_default_l2_gateway(self): - def_bridge_cluster_name = nsx_v3_mocks.NSX_BRIDGE_CLUSTER_NAME - cfg.CONF.set_override("default_bridge_cluster", - def_bridge_cluster_name, - "nsx_v3") - nsx_v3_driver.NsxV3Driver(mock.MagicMock()) - # fake the callback invoked after init - registry.publish(resources.PROCESS, events.BEFORE_SPAWN, - mock.MagicMock()) - l2gws = self.driver._get_l2_gateways(self.context) - def_bridge_cluster_id = ( - self.nsxlib.bridge_cluster.get_id_by_name_or_id( - def_bridge_cluster_name)) - def_l2gw = None - for l2gw in l2gws: - for device in l2gw['devices']: - if device['device_name'] == def_bridge_cluster_id: - def_l2gw = l2gw - self.assertIsNotNone(def_l2gw) - self.assertTrue(def_l2gw.devices[0].device_name, - def_bridge_cluster_id) - self.assertTrue(def_l2gw.devices[0].interfaces[0].interface_name, - 'default-bridge-cluster') - - def test_create_duplicate_default_l2_gateway_noop(self): - def_bridge_cluster_name = nsx_v3_mocks.NSX_BRIDGE_CLUSTER_NAME - cfg.CONF.set_override("default_bridge_cluster", - def_bridge_cluster_name, - "nsx_v3") - for i in range(0, 2): + def_bep_name = NSX_DEFAULT_BEP_NAME + cfg.CONF.set_override("default_bridge_endpoint_profile", + def_bep_name, "nsx_v3") + with mock.patch.object(nsx_v3_driver.NsxV3Driver, + '_get_bridge_vlan_tz_id', + return_value=['some_tz_id']) as mock_get_tz: nsx_v3_driver.NsxV3Driver(mock.MagicMock()) + def_bep_id = ( + self.nsxlib.bridge_endpoint_profile.get_id_by_name_or_id( + def_bep_name)) # fake the callback invoked after init registry.publish(resources.PROCESS, events.BEFORE_SPAWN, - mock.MagicMock()) - l2gws = self.driver._get_l2_gateways(self.context) - # Verify whether only one default L2 gateway is created - self.assertEqual(1, len(l2gws)) + mock.MagicMock()) + l2gws = self.driver._get_l2_gateways(self.context) + def_l2gw = None + for l2gw in l2gws: + for device in l2gw['devices']: + if device['device_name'] == def_bep_id: + def_l2gw = l2gw + self.assertIsNotNone(def_l2gw) + self.assertTrue(def_l2gw.devices[0].device_name, def_bep_id) + self.assertTrue(def_l2gw.devices[0].interfaces[0].interface_name, + 'some_tz_id') + mock_get_tz.assert_called_once_with({'id': def_bep_id, + 'edge_cluster_id': 'meh'}) + + def test_create_duplicate_default_l2_gateway_noop(self): + def_bep_name = NSX_DEFAULT_BEP_NAME + cfg.CONF.set_override("default_bridge_endpoint_profile", + def_bep_name, "nsx_v3") + with mock.patch.object(nsx_v3_driver.NsxV3Driver, + '_get_bridge_vlan_tz_id', + return_value=['some_tz_id']): + for i in range(0, 2): + nsx_v3_driver.NsxV3Driver(mock.MagicMock()) + # fake the callback invoked after init + registry.publish(resources.PROCESS, events.BEFORE_SPAWN, + mock.MagicMock()) + l2gws = self.driver._get_l2_gateways(self.context) + # Verify whether only one default L2 gateway is created + self.assertEqual(1, len(l2gws)) def test_create_default_l2_gateway_no_bc_uuid_noop(self): with mock.patch.object(nsx_v3_driver.NsxV3Driver, 'subscribe_callback_notifications'): nsx_v3_driver.NsxV3Driver(mock.MagicMock()) l2gws = self.driver._get_l2_gateways(self.context) - # Verify no default L2 gateway is created if bridge cluster id is - # not configured in nsx.ini + # Verify no default L2 gateway is created if bridge endpoint + # profile id is not configured in nsx.ini self.assertEqual([], l2gws) def test_create_l2_gateway_multiple_devices_fail(self): @@ -193,28 +217,41 @@ class TestNsxV3L2GatewayDriver(test_l2gw_db.L2GWTestCase, def test_create_l2_gateway_connections_same_params(self): type(self.driver)._core_plugin = self.core_plugin - bc_uuid = uuidutils.generate_uuid() - l2gw_data1 = self._get_l2_gateway_data(name='def-l2gw1', - device_name=bc_uuid) + be_uuid = uuidutils.generate_uuid() + bep_uuid = uuidutils.generate_uuid() + l2gw_data1 = self._get_l2_gateway_data_without_seg_id( + name='def-l2gw1', device_name=bep_uuid) l2gw1 = self._create_l2gateway(l2gw_data1) - l2gw_data2 = self._get_l2_gateway_data(name='def-l2gw2', - device_name=bc_uuid) + l2gw_data2 = self._get_l2_gateway_data_without_seg_id( + name='def-l2gw2', device_name=bep_uuid) l2gw2 = self._create_l2gateway(l2gw_data2) net_data = self._get_nw_data() net = self.core_plugin.create_network(self.context, net_data) l2gw_conn_data1 = {constants.CONNECTION_RESOURCE_NAME: { 'l2_gateway_id': l2gw1['id'], 'tenant_id': 'fake_tenant_id', + 'segmentation_id': '666', 'network_id': net['id']}} - self.l2gw_plugin.create_l2_gateway_connection( - self.context, l2gw_conn_data1) - l2gw_conn_data2 = {constants.CONNECTION_RESOURCE_NAME: { - 'l2_gateway_id': l2gw2['id'], - 'tenant_id': 'fake_tenant_id', - 'network_id': net['id']}} - self.assertRaises(n_exc.InvalidInput, - self.l2gw_plugin.create_l2_gateway_connection, - self.context, l2gw_conn_data2) + # Override "global" mock to return a known id + with mock.patch('vmware_nsxlib.v3.core_resources.' + 'NsxLibBridgeEndpoint.create', + return_value={'id': be_uuid}): + self.l2gw_plugin.create_l2_gateway_connection( + self.context, l2gw_conn_data1) + fake_be = {'id': be_uuid, + 'vlan': 666, + 'bridge_endpoint_profile_id': bep_uuid} + with mock.patch('vmware_nsxlib.v3.NsxLib.' + 'search_all_resource_by_attributes', + return_value=[fake_be]): + l2gw_conn_data2 = {constants.CONNECTION_RESOURCE_NAME: { + 'l2_gateway_id': l2gw2['id'], + 'tenant_id': 'fake_tenant_id', + 'segmentation_id': 666, + 'network_id': net['id']}} + self.assertRaises(n_exc.InvalidInput, + self.l2gw_plugin.create_l2_gateway_connection, + self.context, l2gw_conn_data2) def test_create_l2_gateway_connections_different_bridge(self): type(self.driver)._core_plugin = self.core_plugin @@ -241,6 +278,23 @@ class TestNsxV3L2GatewayDriver(test_l2gw_db.L2GWTestCase, self.l2gw_plugin.create_l2_gateway_connection( self.context, l2gw_conn_data2) + def test_create_l2_gateway_connection_invalid_network_type_fails(self): + type(self.driver)._core_plugin = self.core_plugin + bep_uuid = uuidutils.generate_uuid() + l2gw_data = self._get_l2_gateway_data(name='def-l2gw', + device_name=bep_uuid) + l2gw = self._create_l2gateway(l2gw_data) + net_data = self._get_nw_data(provider=True) + net = self.core_plugin.create_network(self.context, net_data) + l2gw_conn_data = {constants.CONNECTION_RESOURCE_NAME: { + 'l2_gateway_id': l2gw['id'], + 'tenant_id': 'fake_tenant_id', + 'network_id': net['id']}} + self.assertRaises(n_exc.InvalidInput, + self.l2gw_plugin.create_l2_gateway_connection, + self.context, + l2gw_conn_data) + def test_delete_l2_gateway_connection(self): type(self.driver)._core_plugin = self.core_plugin bc_uuid = uuidutils.generate_uuid()