From 06363890eee4f42ce09cbdb0908b9e2f50e1ea02 Mon Sep 17 00:00:00 2001 From: Abhishek Raut Date: Thu, 6 Aug 2015 12:09:06 -0700 Subject: [PATCH] NSXv3: Add backend driver for Layer 2 gateway This patch adds the backend driver to support Layer 2 gateway API calls for NSXv3. Change-Id: Iec1e143115579cca6c8158188217ead4209959bd Partial-bug: #1481087 --- devstack/lib/vmware_nsx_v3 | 3 + etc/nsx.ini | 12 + tox.ini | 3 +- .../alembic_migrations/versions/HEADS | 2 +- ...70ac3ae8_nsxv3_add_l2gwconnection_table.py | 43 +++ .../neutron/plugins/vmware/common/config.py | 4 + .../plugins/vmware/common/exceptions.py | 4 + .../plugins/vmware/common/nsx_constants.py | 3 + .../neutron/plugins/vmware/dbexts/db.py | 21 ++ .../plugins/vmware/dbexts/nsx_models.py | 14 + .../plugins/vmware/nsxlib/v3/__init__.py | 27 ++ .../plugins/vmware/plugins/nsx_v3_plugin.py | 60 +++- .../services/l2gateway/nsx_v3_driver.py | 266 ++++++++++++++++++ .../neutron/tests/unit/vmware/__init__.py | 2 + .../tests/unit/vmware/services/__init__.py | 0 .../vmware/services/l2gateway/__init__.py | 0 .../services/l2gateway/test_nsxv3_driver.py | 74 +++++ 17 files changed, 523 insertions(+), 15 deletions(-) create mode 100644 vmware_nsx/neutron/db/migration/alembic_migrations/versions/liberty/expand/279b70ac3ae8_nsxv3_add_l2gwconnection_table.py create mode 100644 vmware_nsx/neutron/services/l2gateway/nsx_v3_driver.py create mode 100644 vmware_nsx/neutron/tests/unit/vmware/services/__init__.py create mode 100644 vmware_nsx/neutron/tests/unit/vmware/services/l2gateway/__init__.py create mode 100644 vmware_nsx/neutron/tests/unit/vmware/services/l2gateway/test_nsxv3_driver.py diff --git a/devstack/lib/vmware_nsx_v3 b/devstack/lib/vmware_nsx_v3 index db83a04861..6fe3c224d9 100644 --- a/devstack/lib/vmware_nsx_v3 +++ b/devstack/lib/vmware_nsx_v3 @@ -106,6 +106,9 @@ function neutron_plugin_configure_service { else die $LINENO "The VMware NSX plugin needs at least an NSX controller." fi + if [[ "$NSX_L2GW_DRIVER" != "" ]]; then + iniset /$Q_PLUGIN_CONF_FILE nsx nsx_l2gw_driver $NSX_L2GW_DRIVER + fi _nsxv3_ini_set default_tier0_router_uuid $DEFAULT_TIER0_ROUTER_UUID _nsxv3_ini_set nsx_manager $NSX_MANAGER "The VMWare NSX plugin needs a NSX manager." _nsxv3_ini_set nsx_user $NSX_USER diff --git a/etc/nsx.ini b/etc/nsx.ini index 7fdb3f0249..93f2fbdcff 100644 --- a/etc/nsx.ini +++ b/etc/nsx.ini @@ -225,6 +225,10 @@ # "service". # replication_mode = service +# Specify the class path for the Layer 2 gateway backend driver(i.e. NSXv3/NSX-V). +# This field will be used when a L2 Gateway service plugin is configured. +# nsx_l2gw_driver = vmware_nsx.neutron.services.l2gateway.nsx_v3_driver.NsxV3Driver + [nsx_sync] # Interval in seconds between runs of the status synchronization task. # The plugin will aim at resynchronizing operational status for all @@ -327,3 +331,11 @@ # UUID of the default tier0 router that will be used for connecting to # tier1 logical routers and configuring external network # default_tier0_router_uuid = 412983fd-9016-45e5-93f2-48ba2a931225 + +# UUID of the default NSX bridge cluster that will be used to perform +# L2 gateway bridging between VXLAN and VLAN networks. It is an optional +# field. If default bridge cluster UUID is not specified, admin will have to +# manually create a L2 gateway corresponding to a NSX Bridge Cluster using +# L2 gateway APIs. +# This field must be specified on one of the active neutron servers only. +# default_bridge_cluster_uuid = diff --git a/tox.ini b/tox.ini index e058722318..6ad0136d1f 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,8 @@ setenv = VIRTUAL_ENV={envdir} PYTHONHASHSEED=0 usedevelop = True install_command = {toxinidir}/tools/tox_install.sh {opts} {packages} -deps = -r{toxinidir}/requirements.txt +deps = -egit+https://git.openstack.org/openstack/networking-l2gw#egg=networking-l2gw + -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt whitelist_externals = sh commands = diff --git a/vmware_nsx/neutron/db/migration/alembic_migrations/versions/HEADS b/vmware_nsx/neutron/db/migration/alembic_migrations/versions/HEADS index 30d4e4e270..3ce48c43cd 100644 --- a/vmware_nsx/neutron/db/migration/alembic_migrations/versions/HEADS +++ b/vmware_nsx/neutron/db/migration/alembic_migrations/versions/HEADS @@ -1,2 +1,2 @@ -28430956782d +279b70ac3ae8 393bf843b96 diff --git a/vmware_nsx/neutron/db/migration/alembic_migrations/versions/liberty/expand/279b70ac3ae8_nsxv3_add_l2gwconnection_table.py b/vmware_nsx/neutron/db/migration/alembic_migrations/versions/liberty/expand/279b70ac3ae8_nsxv3_add_l2gwconnection_table.py new file mode 100644 index 0000000000..ae7d0f8f6c --- /dev/null +++ b/vmware_nsx/neutron/db/migration/alembic_migrations/versions/liberty/expand/279b70ac3ae8_nsxv3_add_l2gwconnection_table.py @@ -0,0 +1,43 @@ +# Copyright 2015 OpenStack Foundation +# +# 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. +# + +"""NSXv3 Add l2gwconnection table + +Revision ID: 279b70ac3ae8 +Revises: 28430956782d +Create Date: 2015-08-14 02:04:09.807926 + +""" + +# revision identifiers, used by Alembic. +revision = '279b70ac3ae8' +down_revision = '28430956782d' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table( + 'nsx_l2gw_connection_mappings', + sa.Column('connection_id', sa.String(length=36), nullable=False), + sa.Column('port_id', sa.String(length=36), nullable=False), + sa.Column('bridge_endpoint_id', sa.String(length=36), nullable=False), + sa.ForeignKeyConstraint(['connection_id'], + ['l2gatewayconnections.id'], + ondelete='CASCADE'), + sa.ForeignKeyConstraint(['port_id'], ['ports.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('connection_id'), + ) diff --git a/vmware_nsx/neutron/plugins/vmware/common/config.py b/vmware_nsx/neutron/plugins/vmware/common/config.py index 38262dd61d..e0de08bf7f 100644 --- a/vmware_nsx/neutron/plugins/vmware/common/config.py +++ b/vmware_nsx/neutron/plugins/vmware/common/config.py @@ -183,6 +183,10 @@ nsx_v3_opts = [ "starting Neutron with the NSX plugin.")), cfg.StrOpt('default_edge_cluster_uuid', help=_("Default edge cluster identifier")), + cfg.StrOpt('default_bridge_cluster_uuid', + help=_("Default bridge cluster identifier for L2 gateway. " + "This needs to be created in NSX before using the L2 " + "gateway service plugin.")), cfg.IntOpt('retries', default=10, help=_('Maximum number of times to retry API request')), diff --git a/vmware_nsx/neutron/plugins/vmware/common/exceptions.py b/vmware_nsx/neutron/plugins/vmware/common/exceptions.py index 8ff3cd8bea..ad25019b6b 100644 --- a/vmware_nsx/neutron/plugins/vmware/common/exceptions.py +++ b/vmware_nsx/neutron/plugins/vmware/common/exceptions.py @@ -137,3 +137,7 @@ class ResourceNotFound(ManagerError): class StaleRevision(ManagerError): pass + + +class NsxL2GWConnectionMappingNotFound(n_exc.NotFound): + message = _('Unable to find mapping for L2 gateway connection: %(conn)s') diff --git a/vmware_nsx/neutron/plugins/vmware/common/nsx_constants.py b/vmware_nsx/neutron/plugins/vmware/common/nsx_constants.py index b5ff6c348a..91ad6505e7 100644 --- a/vmware_nsx/neutron/plugins/vmware/common/nsx_constants.py +++ b/vmware_nsx/neutron/plugins/vmware/common/nsx_constants.py @@ -41,3 +41,6 @@ ROUTER_TYPES = [ROUTER_TYPE_TIER0, ROUTER_TYPE_TIER1] # L2 agent vif type VIF_TYPE_DVS = 'dvs' + +# NSXv3 L2 Gateway constants +BRIDGE_ENDPOINT = "BRIDGEENDPOINT" diff --git a/vmware_nsx/neutron/plugins/vmware/dbexts/db.py b/vmware_nsx/neutron/plugins/vmware/dbexts/db.py index 0ab6371cba..a437c0640a 100644 --- a/vmware_nsx/neutron/plugins/vmware/dbexts/db.py +++ b/vmware_nsx/neutron/plugins/vmware/dbexts/db.py @@ -21,6 +21,7 @@ from sqlalchemy.orm import exc import neutron.db.api as db +from vmware_nsx.neutron.plugins.vmware.common import exceptions as nsx_exc from vmware_nsx.neutron.plugins.vmware.dbexts import nsx_models LOG = logging.getLogger(__name__) @@ -203,3 +204,23 @@ def is_multiprovider_network(session, network_id): return bool( session.query(nsx_models.MultiProviderNetworks).filter_by( network_id=network_id).first()) + + +# NSXv3 L2 Gateway DB methods. +def add_l2gw_connection_mapping(session, connection_id, bridge_endpoint_id, + port_id): + with session.begin(subtransactions=True): + mapping = nsx_models.NsxL2GWConnectionMapping( + connection_id=connection_id, + port_id=port_id, + bridge_endpoint_id=bridge_endpoint_id) + session.add(mapping) + return mapping + + +def get_l2gw_connection_mapping(session, connection_id): + try: + return (session.query(nsx_models.NsxL2GWConnectionMapping). + filter_by(connection_id=connection_id).one()) + except exc.NoResultFound: + raise nsx_exc.NsxL2GWConnectionMappingNotFound(conn=connection_id) diff --git a/vmware_nsx/neutron/plugins/vmware/dbexts/nsx_models.py b/vmware_nsx/neutron/plugins/vmware/dbexts/nsx_models.py index db71822df1..c32f001794 100644 --- a/vmware_nsx/neutron/plugins/vmware/dbexts/nsx_models.py +++ b/vmware_nsx/neutron/plugins/vmware/dbexts/nsx_models.py @@ -300,3 +300,17 @@ class NetworkQueueMapping(model_base.BASEV2): models_v2.Network, backref=orm.backref("qos_queue", uselist=False, cascade='delete', lazy='joined')) + + +class NsxL2GWConnectionMapping(model_base.BASEV2): + """Define a mapping between L2 gateway connection and bridge endpoint.""" + __tablename__ = 'nsx_l2gw_connection_mappings' + connection_id = sa.Column(sa.String(36), + sa.ForeignKey("l2gatewayconnections.id", + ondelete="CASCADE"), + nullable=False, + primary_key=True) + port_id = sa.Column(sa.String(36), + sa.ForeignKey("ports.id", ondelete="CASCADE"), + nullable=False) + bridge_endpoint_id = sa.Column(sa.String(36), nullable=False) diff --git a/vmware_nsx/neutron/plugins/vmware/nsxlib/v3/__init__.py b/vmware_nsx/neutron/plugins/vmware/nsxlib/v3/__init__.py index a270f40388..a4a10ce2d5 100644 --- a/vmware_nsx/neutron/plugins/vmware/nsxlib/v3/__init__.py +++ b/vmware_nsx/neutron/plugins/vmware/nsxlib/v3/__init__.py @@ -282,3 +282,30 @@ def get_qos_switching_profile(profile_id): def delete_qos_switching_profile(profile_id): resource = 'switching-profiles/%s' % profile_id client.delete_resource(resource) + + +def create_bridge_endpoint(device_name, seg_id, tags): + """Create a bridge endpoint on the backend. + + Create a bridge endpoint resource on a bridge cluster for the L2 gateway + network connection. + :param device_name: device_name actually refers to the bridge cluster's + UUID. + :param seg_id: integer representing the VLAN segmentation ID. + :param tags: nsx backend specific tags. + """ + resource = 'bridge-endpoints' + body = {'bridge_cluster_id': device_name, + 'tags': tags, + 'vlan': seg_id} + return client.create_resource(resource, body) + + +def delete_bridge_endpoint(bridge_endpoint_id): + """Delete a bridge endpoint on the backend. + + :param bridge_endpoint_id: string representing the UUID of the bridge + endpoint to be deleted. + """ + resource = 'bridge-endpoints/%s' % bridge_endpoint_id + client.delete_resource(resource) diff --git a/vmware_nsx/neutron/plugins/vmware/plugins/nsx_v3_plugin.py b/vmware_nsx/neutron/plugins/vmware/plugins/nsx_v3_plugin.py index 6740907c0a..c791c9fe65 100644 --- a/vmware_nsx/neutron/plugins/vmware/plugins/nsx_v3_plugin.py +++ b/vmware_nsx/neutron/plugins/vmware/plugins/nsx_v3_plugin.py @@ -27,13 +27,10 @@ from neutron.api.rpc.agentnotifiers import dhcp_rpc_agent_api from neutron.api.rpc.handlers import dhcp_rpc from neutron.api.rpc.handlers import metadata_rpc from neutron.api.v2 import attributes -from neutron.extensions import external_net as ext_net_extn -from neutron.extensions import extra_dhcp_opt as edo_ext -from neutron.extensions import l3 -from neutron.extensions import portbindings as pbin -from neutron.extensions import providernet as pnet -from neutron.extensions import securitygroup as ext_sg - +from neutron.callbacks import events +from neutron.callbacks import exceptions as callback_exc +from neutron.callbacks import registry +from neutron.callbacks import resources from neutron.common import constants as const from neutron.common import exceptions as n_exc from neutron.common import rpc as n_rpc @@ -49,12 +46,19 @@ from neutron.db import l3_gwmode_db from neutron.db import models_v2 from neutron.db import portbindings_db from neutron.db import securitygroups_db +from neutron.extensions import external_net as ext_net_extn +from neutron.extensions import extra_dhcp_opt as ext_edo +from neutron.extensions import l3 +from neutron.extensions import portbindings as pbin +from neutron.extensions import providernet as pnet +from neutron.extensions import securitygroup as ext_sg from neutron.i18n import _LE, _LI, _LW from neutron.plugins.common import constants as plugin_const from neutron.plugins.common import utils as n_utils from vmware_nsx.neutron.plugins.vmware.common import config # noqa from vmware_nsx.neutron.plugins.vmware.common import exceptions as nsx_exc +from vmware_nsx.neutron.plugins.vmware.common import nsx_constants from vmware_nsx.neutron.plugins.vmware.common import utils from vmware_nsx.neutron.plugins.vmware.dbexts import db as nsx_db from vmware_nsx.neutron.plugins.vmware.nsxlib import v3 as nsxlib @@ -429,18 +433,28 @@ class NsxV3Plugin(db_base_plugin_v2.NeutronDbPluginV2, # self.get_port(context, parent_name) return parent_name, tag - def _create_port_at_the_backend(self, context, neutron_db, port_data): + def _create_port_at_the_backend(self, context, neutron_db, + port_data, l2gw_port_check): tags = utils.build_v3_tags_payload(port_data) parent_name, tag = self._get_data_from_binding_profile( context, port_data) address_bindings = self._build_address_bindings(port_data) # FIXME(arosen): we might need to pull this out of the # transaction here later. + vif_uuid = port_data['id'] + attachment_type = nsx_constants.ATTACHMENT_VIF + # Change the attachment type for L2 gateway owned ports. + if l2gw_port_check: + # NSX backend requires the vif id be set to bridge endpoint id + # for ports plugged into a Bridge Endpoint. + vif_uuid = port_data.get('device_id') + attachment_type = port_data.get('device_owner') result = nsxlib.create_logical_port( lswitch_id=port_data['network_id'], - vif_uuid=port_data['id'], name=port_data['name'], tags=tags, + vif_uuid=vif_uuid, name=port_data['name'], tags=tags, admin_state=port_data['admin_state_up'], address_bindings=address_bindings, + attachment_type=attachment_type, parent_name=parent_name, parent_tag=tag) # TODO(salv-orlando): The logical switch identifier in the @@ -450,8 +464,8 @@ class NsxV3Plugin(db_base_plugin_v2.NeutronDbPluginV2, neutron_db['network_id'], result['id']) return result - def create_port(self, context, port): - dhcp_opts = port['port'].get(edo_ext.EXTRADHCPOPTS, []) + def create_port(self, context, port, l2gw_port_check=False): + dhcp_opts = port['port'].get(ext_edo.EXTRADHCPOPTS, []) port_id = uuidutils.generate_uuid() port['port']['id'] = port_id @@ -468,7 +482,7 @@ class NsxV3Plugin(db_base_plugin_v2.NeutronDbPluginV2, if not self._network_is_external( context, port['port']['network_id']): lport = self._create_port_at_the_backend( - context, neutron_db, port['port']) + context, neutron_db, port['port'], l2gw_port_check) self._process_portbindings_create_and_update(context, port['port'], neutron_db) @@ -488,7 +502,27 @@ class NsxV3Plugin(db_base_plugin_v2.NeutronDbPluginV2, context, lport['id'], [], sgids) return neutron_db - def delete_port(self, context, port_id, l3_port_check=True): + def _pre_delete_port_check(self, context, port_id, l2gw_port_check): + """Perform checks prior to deleting a port.""" + try: + kwargs = { + 'context': context, + 'port_check': l2gw_port_check, + 'port_id': port_id, + } + # Send delete port notification to any interested service plugin + registry.notify( + resources.PORT, events.BEFORE_DELETE, self, **kwargs) + except callback_exc.CallbackFailure as e: + if len(e.errors) == 1: + raise e.errors[0].error + raise n_exc.ServicePortInUse(port_id=port_id, reason=e) + + def delete_port(self, context, port_id, + l3_port_check=True, l2gw_port_check=True): + # if needed, check to see if this is a port owned by + # a l2 gateway. If so, we should prevent deletion here + self._pre_delete_port_check(context, port_id, l2gw_port_check) # if needed, check to see if this is a port owned by # a l3 router. If so, we should prevent deletion here if l3_port_check: diff --git a/vmware_nsx/neutron/services/l2gateway/nsx_v3_driver.py b/vmware_nsx/neutron/services/l2gateway/nsx_v3_driver.py new file mode 100644 index 0000000000..ca01349d4c --- /dev/null +++ b/vmware_nsx/neutron/services/l2gateway/nsx_v3_driver.py @@ -0,0 +1,266 @@ +# Copyright 2015 VMware, Inc. +# +# All Rights Reserved +# +# 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 networking_l2gw.db.l2gateway import l2gateway_db +from networking_l2gw.services.l2gateway.common import constants as l2gw_const +from networking_l2gw.services.l2gateway import exceptions as l2gw_exc +from oslo_config import cfg +from oslo_db import exception as db_exc +from oslo_log import log as logging +from oslo_utils import excutils +from oslo_utils import uuidutils + +from neutron.api.v2 import attributes +from neutron.callbacks import events +from neutron.callbacks import registry +from neutron.callbacks import resources +from neutron.common import exceptions as n_exc +from neutron import context +from neutron.extensions import providernet +from neutron.i18n import _LE, _LI +from neutron import manager +from neutron.plugins.common import utils as n_utils + +from vmware_nsx.neutron.plugins.vmware.common import exceptions as nsx_exc +from vmware_nsx.neutron.plugins.vmware.common import nsx_constants +from vmware_nsx.neutron.plugins.vmware.common import utils as nsx_utils +from vmware_nsx.neutron.plugins.vmware.dbexts import db as nsx_db +from vmware_nsx.neutron.plugins.vmware.nsxlib import v3 as nsxlib + +LOG = logging.getLogger(__name__) + + +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): + # Create a default L2 gateway if default_bridge_cluster_uuid is + # provided in nsx.ini + self._ensure_default_l2_gateway() + self.subscribe_callback_notifications() + LOG.debug("Initialization complete for NSXv3 driver for " + "L2 gateway service plugin.") + + @property + def _core_plugin(self): + return manager.NeutronManager.get_plugin() + + def subscribe_callback_notifications(self): + registry.subscribe(self._prevent_l2gw_port_delete, resources.PORT, + events.BEFORE_DELETE) + + def _ensure_default_l2_gateway(self): + """ + Create a default logical L2 gateway. + + Create a logical L2 gateway in the neutron database if the + default_bridge_cluster_uuid config parameter is set and if it is + not previously created. If not set, return. + """ + def_l2gw_uuid = cfg.CONF.nsx_v3.default_bridge_cluster_uuid + # Return if no default_bridge_cluster_uuid set in config + if not def_l2gw_uuid: + LOG.info(_LI("NSX: Default bridge cluster UUID not configured " + "in nsx.ini. No default L2 gateway created.")) + return + admin_ctx = context.get_admin_context() + # Optimistically create the default L2 gateway in neutron DB + device = {'device_name': def_l2gw_uuid, + 'interfaces': [{'name': 'default-bridge-cluster'}]} + def_l2gw = {'name': 'default-l2gw', + 'devices': [device]} + l2gw_dict = {self.gateway_resource: def_l2gw} + l2_gateway = 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(_LI("Default L2 gateway is already created.")) + try: + # Try deleting this duplicate default L2 gateway + 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. + self.delete_l2_gateway(admin_ctx, l2_gateway['id']) + else: + def_l2gw_exists = True + return l2_gateway + + def _prevent_l2gw_port_delete(self, resource, event, trigger, **kwargs): + context = kwargs.get('context') + port_id = kwargs.get('port_id') + port_check = kwargs.get('port_check') + 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. + # 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. + if len(devices) != 1: + msg = _("Only a single device is supported for one L2 gateway") + raise n_exc.InvalidInput(error_message=msg) + if not uuidutils.is_uuid_like(devices[0]['device_name']): + msg = _("Device name must be configured with a UUID") + raise n_exc.InvalidInput(error_message=msg) + + def create_l2_gateway(self, context, l2_gateway): + """Create a logical L2 gateway.""" + gw = l2_gateway[self.gateway_resource] + devices = gw['devices'] + self._validate_device_list(devices) + return super(NsxV3Driver, self).create_l2_gateway(context, + l2_gateway) + + 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") % + network_type) + raise n_exc.InvalidInput(error_message=msg) + + def _validate_segment_id(self, seg_id): + if not seg_id: + raise l2gw_exc.L2GatewaySegmentationRequired + return n_utils.is_valid_vlan_tag(seg_id) + + def create_l2_gateway_connection(self, context, l2_gateway_connection): + """Create a L2 gateway connection.""" + #TODO(abhiraut): Move backend logic in a separate method + gw_connection = l2_gateway_connection.get(l2gw_const. + CONNECTION_RESOURCE_NAME) + network_id = gw_connection.get(l2gw_const.NETWORK_ID) + self._validate_network(context, network_id) + l2gw_connection = super( + NsxV3Driver, self).create_l2_gateway_connection( + context, l2_gateway_connection) + l2gw_id = gw_connection.get(l2gw_const.L2GATEWAY_ID) + devices = self._get_l2_gateway_devices(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. + device_name = devices[0].get('device_name') + # The seg-id will be provided either during gateway create or gateway + # connection create. l2gateway_db_mixin makes sure that it is + # configured one way or the other. + seg_id = gw_connection.get(l2gw_const.SEG_ID) + if seg_id is None: + seg_id = devices[0]['interfaces'][0].get('segmentation_id') + self._validate_segment_id(seg_id) + try: + tags = nsx_utils.build_v3_tags_payload(gw_connection) + bridge_endpoint = nsxlib.create_bridge_endpoint( + device_name=device_name, + seg_id=seg_id, + tags=tags) + except nsx_exc.ManagerError: + LOG.exception(_LE("Unable to update NSX backend, rolling back " + "changes on neutron")) + with excutils.save_and_reraise_exception(): + super(NsxV3Driver, + self).delete_l2_gateway_connection(context, + l2gw_connection['id']) + # Create a logical port and connect it to the bridge endpoint. + tenant_id = self._core_plugin._get_tenant_id_for_create(context, + gw_connection) + # _get_tenant_id_for_create might return None in some cases. + # This is not acceptable for the NSX plugin + if context.is_admin and not tenant_id: + tenant_id = context.tenant_id + #TODO(abhiraut): Consider specifying the name of the port + port_dict = {'port': { + 'tenant_id': tenant_id, + 'network_id': network_id, + 'mac_address': attributes.ATTR_NOT_SPECIFIED, + 'admin_state_up': True, + 'fixed_ips': [], + 'device_id': bridge_endpoint['id'], + 'device_owner': nsx_constants.BRIDGE_ENDPOINT, + 'name': '', }} + try: + port = self._core_plugin.create_port(context, port_dict) + # Deallocate IP address from the port. + for fixed_ip in port.get('fixed_ips', []): + self._core_plugin._delete_ip_allocation(context, network_id, + fixed_ip['subnet_id'], + fixed_ip['ip_address']) + LOG.debug("IP addresses deallocated on port %s", port['id']) + except (nsx_exc.ManagerError, + n_exc.NeutronException): + with excutils.save_and_reraise_exception(): + LOG.exception(_LE("Unable to create L2 gateway port, " + "rolling back changes on neutron")) + nsxlib.delete_bridge_endpoint(bridge_endpoint['id']) + super(NsxV3Driver, + self).delete_l2_gateway_connection(context, + l2gw_connection['id']) + try: + # Update neutron's database with the mappings. + nsx_db.add_l2gw_connection_mapping( + session=context.session, + connection_id=l2gw_connection['id'], + bridge_endpoint_id=bridge_endpoint['id'], + port_id=port['id']) + except db_exc.DBError: + with excutils.save_and_reraise_exception(): + LOG.exception(_LE("Unable to add L2 gateway connection " + "mappings, rolling back changes on neutron")) + nsxlib.delete_bridge_endpoint(bridge_endpoint['id']) + super(NsxV3Driver, + self).delete_l2_gateway_connection(context, + l2gw_connection['id']) + return l2gw_connection + + def delete_l2_gateway_connection(self, context, l2_gateway_connection): + """Delete a L2 gateway connection.""" + conn_mapping = nsx_db.get_l2gw_connection_mapping( + session=context.session, + connection_id=l2_gateway_connection) + bridge_endpoint_id = conn_mapping.get('bridge_endpoint_id') + # Delete the logical port from the bridge endpoint. + self._core_plugin.delete_port(context=context, + port_id=conn_mapping.get('port_id'), + l2gw_port_check=False) + try: + nsxlib.delete_bridge_endpoint(bridge_endpoint_id) + except nsx_exc.ManagerError: + LOG.exception(_LE("Unable to delete bridge endpoint %s on the " + "backend.") % bridge_endpoint_id) + return (super(NsxV3Driver, self). + delete_l2_gateway_connection(context, + l2_gateway_connection)) + + def prevent_l2gw_port_deletion(self, context, port_id): + """Prevent core plugin from deleting L2 gateway port.""" + try: + port = self._core_plugin.get_port(context, port_id) + except n_exc.PortNotFound: + return + if port['device_owner'] == nsx_constants.BRIDGE_ENDPOINT: + reason = _("has device owner %s") % port['device_owner'] + raise n_exc.ServicePortInUse(port_id=port_id, reason=reason) diff --git a/vmware_nsx/neutron/tests/unit/vmware/__init__.py b/vmware_nsx/neutron/tests/unit/vmware/__init__.py index bdd17ae422..7f41696ac9 100644 --- a/vmware_nsx/neutron/tests/unit/vmware/__init__.py +++ b/vmware_nsx/neutron/tests/unit/vmware/__init__.py @@ -16,6 +16,8 @@ import os +from networking_l2gw.db.l2gateway import l2gateway_models # noqa + from vmware_nsx.neutron.plugins.vmware.api_client import client as nsx_client from vmware_nsx.neutron.plugins.vmware.api_client import eventlet_client from vmware_nsx.neutron.plugins.vmware import extensions diff --git a/vmware_nsx/neutron/tests/unit/vmware/services/__init__.py b/vmware_nsx/neutron/tests/unit/vmware/services/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/neutron/tests/unit/vmware/services/l2gateway/__init__.py b/vmware_nsx/neutron/tests/unit/vmware/services/l2gateway/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/neutron/tests/unit/vmware/services/l2gateway/test_nsxv3_driver.py b/vmware_nsx/neutron/tests/unit/vmware/services/l2gateway/test_nsxv3_driver.py new file mode 100644 index 0000000000..38c17b549c --- /dev/null +++ b/vmware_nsx/neutron/tests/unit/vmware/services/l2gateway/test_nsxv3_driver.py @@ -0,0 +1,74 @@ +# Copyright (c) 2015 VMware, Inc. +# +# 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. +import mock + +from neutron import context +from neutron.tests import base + +from networking_l2gw.tests.unit.db import test_l2gw_db + +from vmware_nsx.neutron.services.l2gateway import nsx_v3_driver +from vmware_nsx.neutron.services.l2gateway import plugin as l2gw_plugin + +from oslo_config import cfg +from oslo_utils import uuidutils + + +NSX_V3_DRIVER_CLASS_PATH = ('vmware_nsx.neutron.services.l2gateway.' + 'nsx_v3_driver.NsxV3Driver') + + +class TestNsxV3L2GatewayDriver(test_l2gw_db.L2GWTestCase, + base.BaseTestCase): + + def setUp(self): + super(TestNsxV3L2GatewayDriver, self).setUp() + cfg.CONF.set_override("nsx_l2gw_driver", + NSX_V3_DRIVER_CLASS_PATH, 'NSX') + self.l2gw_plugin = l2gw_plugin.NsxL2GatewayPlugin() + self.driver = nsx_v3_driver.NsxV3Driver() + self.context = context.get_admin_context() + + def test_nsxl2gw_driver_init(self): + with mock.patch.object(nsx_v3_driver.NsxV3Driver, + '_ensure_default_l2_gateway') as def_gw: + with mock.patch.object(nsx_v3_driver.NsxV3Driver, + 'subscribe_callback_notifications') as sub: + with mock.patch.object(nsx_v3_driver.LOG, + 'debug') as debug: + l2gw_plugin.NsxL2GatewayPlugin() + self.assertTrue(def_gw.called) + self.assertTrue(sub.called) + self.assertTrue(debug.called) + + def test_create_default_l2_gateway(self): + def_bridge_cluster_id = uuidutils.generate_uuid() + with mock.patch.object(nsx_v3_driver.NsxV3Driver, + 'subscribe_callback_notifications'): + cfg.CONF.set_override("default_bridge_cluster_uuid", + def_bridge_cluster_id, + "nsx_v3") + l2gw_plugin.NsxL2GatewayPlugin() + 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_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')