From 10dbaab0b43392a92911ca43807862f9c015b0b3 Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Mon, 17 Feb 2014 16:20:53 -0800 Subject: [PATCH] Add support for tenant-provided NSX gateways devices Add a new API resource specific to the NSX plugin for registering tenant-owned NSX gateway devices with the NSX controller. This API also allows tenants for managing gateway devices on the NSX backend. The behaviour of the net-gateway extension is mostly unchanged with the only difference that newly created network gateways can now only refer exlusively gateway devices registered using the API resource introduced with this patch. Implements blueprint nsx-remote-net-gw-integration Change-Id: Ia2bdd0164498fe46a027b1d8f5a9d9f4e37558a4 --- neutron/api/v2/resource_helper.py | 20 +- .../versions/19180cf98af6_nsx_gw_devices.py | 100 +++ neutron/plugins/vmware/common/nsx_utils.py | 36 + neutron/plugins/vmware/common/utils.py | 11 + neutron/plugins/vmware/dbexts/networkgw_db.py | 128 +++- .../plugins/vmware/extensions/networkgw.py | 104 ++- neutron/plugins/vmware/nsxlib/l2gateway.py | 104 ++- neutron/plugins/vmware/plugins/base.py | 230 ++++++- .../unit/vmware/extensions/test_networkgw.py | 614 +++++++++++++----- .../unit/vmware/nsxlib/test_l2gateway.py | 149 +++++ 10 files changed, 1281 insertions(+), 215 deletions(-) create mode 100644 neutron/db/migration/alembic_migrations/versions/19180cf98af6_nsx_gw_devices.py diff --git a/neutron/api/v2/resource_helper.py b/neutron/api/v2/resource_helper.py index d908c5ba0f..ab90bd683b 100644 --- a/neutron/api/v2/resource_helper.py +++ b/neutron/api/v2/resource_helper.py @@ -46,11 +46,29 @@ def build_resource_info(plural_mappings, resource_map, which_service, API resource objects for advanced services extensions. Will optionally translate underscores to dashes in resource names, register the resource, and accept action information for resources. + + :param plural_mappings: mappings between singular and plural forms + :param resource_map: attribute map for the WSGI resources to create + :param which_service: The name of the service for which the WSGI resources + are being created. This name will be used to pass + the appropriate plugin to the WSGI resource. + It can be set to None or "CORE"to create WSGI + resources for the the core plugin + :param action_map: custom resource actions + :param register_quota: it can be set to True to register quotas for the + resource(s) being created + :param translate_name: replaces underscores with dashes + :param allow_bulk: True if bulk create are allowed """ resources = [] + if not which_service: + which_service = constants.CORE if action_map is None: action_map = {} - plugin = manager.NeutronManager.get_service_plugins()[which_service] + if which_service != constants.CORE: + plugin = manager.NeutronManager.get_service_plugins()[which_service] + else: + plugin = manager.NeutronManager.get_plugin() for collection_name in resource_map: resource_name = plural_mappings[collection_name] params = resource_map.get(collection_name, {}) diff --git a/neutron/db/migration/alembic_migrations/versions/19180cf98af6_nsx_gw_devices.py b/neutron/db/migration/alembic_migrations/versions/19180cf98af6_nsx_gw_devices.py new file mode 100644 index 0000000000..1e5ecc5ba9 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/19180cf98af6_nsx_gw_devices.py @@ -0,0 +1,100 @@ +# Copyright 2014 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. +# + +"""nsx_gw_devices + +Revision ID: 19180cf98af6 +Revises: 117643811bca +Create Date: 2014-02-26 02:46:26.151741 + +""" + +# revision identifiers, used by Alembic. +revision = '19180cf98af6' +down_revision = '117643811bca' + +# Change to ['*'] if this migration applies to all plugins + +migration_for_plugins = [ + 'neutron.plugins.nicira.NeutronPlugin.NvpPluginV2', + 'neutron.plugins.nicira.NeutronServicePlugin.NvpAdvancedPlugin', + 'neutron.plugins.vmware.plugin.NsxPlugin', + 'neutron.plugins.vmware.plugin.NsxServicePlugin' +] + +from alembic import op +import sqlalchemy as sa + +from neutron.db import migration + + +def upgrade(active_plugins=None, options=None): + if not migration.should_run(active_plugins, migration_for_plugins): + return + + op.create_table( + 'networkgatewaydevicereferences', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('network_gateway_id', sa.String(length=36), nullable=True), + sa.Column('interface_name', sa.String(length=64), nullable=True), + sa.ForeignKeyConstraint(['network_gateway_id'], ['networkgateways.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + mysql_engine='InnoDB') + # Copy data from networkgatewaydevices into networkgatewaydevicereference + op.execute("INSERT INTO networkgatewaydevicereferences SELECT " + "id, network_gateway_id, interface_name FROM " + "networkgatewaydevices") + # drop networkgatewaydevices + op.drop_table('networkgatewaydevices') + op.create_table( + 'networkgatewaydevices', + sa.Column('tenant_id', sa.String(length=255), nullable=True), + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('nsx_id', sa.String(length=36), nullable=True), + sa.Column('name', sa.String(length=255), nullable=True), + sa.Column('connector_type', sa.String(length=10), nullable=True), + sa.Column('connector_ip', sa.String(length=64), nullable=True), + sa.Column('status', sa.String(length=16), nullable=True), + sa.PrimaryKeyConstraint('id'), + mysql_engine='InnoDB') + # Create a networkgatewaydevice for each existing reference. + # For existing references nsx_id == neutron_id + # Do not fill conenctor info as they would be unknown + op.execute("INSERT INTO networkgatewaydevices (id, nsx_id) SELECT " + "id, id as nsx_id FROM networkgatewaydevicereferences") + + +def downgrade(active_plugins=None, options=None): + if not migration.should_run(active_plugins, migration_for_plugins): + return + + op.drop_table('networkgatewaydevices') + # Re-create previous version of networkgatewaydevices table + op.create_table( + 'networkgatewaydevices', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('network_gateway_id', sa.String(length=36), nullable=True), + sa.Column('interface_name', sa.String(length=64), nullable=True), + sa.ForeignKeyConstraint(['network_gateway_id'], ['networkgateways.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + mysql_engine='InnoDB') + # Copy from networkgatewaydevicereferences to networkgatewaydevices + op.execute("INSERT INTO networkgatewaydevices SELECT " + "id, network_gateway_id, interface_name FROM " + "networkgatewaydevicereferences") + # Dropt networkgatewaydevicereferences + op.drop_table('networkgatewaydevicereferences') diff --git a/neutron/plugins/vmware/common/nsx_utils.py b/neutron/plugins/vmware/common/nsx_utils.py index 55d8b74752..c2c2b7f28c 100644 --- a/neutron/plugins/vmware/common/nsx_utils.py +++ b/neutron/plugins/vmware/common/nsx_utils.py @@ -15,10 +15,14 @@ # License for the specific language governing permissions and limitations # under the License. +from neutron.common import exceptions as n_exc from neutron.openstack.common import log from neutron.plugins.vmware.api_client import client +from neutron.plugins.vmware.api_client import exception as api_exc from neutron.plugins.vmware.dbexts import db as nsx_db +from neutron.plugins.vmware.dbexts import networkgw_db from neutron.plugins.vmware import nsx_cluster +from neutron.plugins.vmware.nsxlib import l2gateway as l2gwlib from neutron.plugins.vmware.nsxlib import router as routerlib from neutron.plugins.vmware.nsxlib import secgroup as secgrouplib from neutron.plugins.vmware.nsxlib import switch as switchlib @@ -211,3 +215,35 @@ def create_nsx_cluster(cluster_opts, concurrent_connections, gen_timeout): concurrent_connections=concurrent_connections, gen_timeout=gen_timeout) return cluster + + +def get_nsx_device_status(cluster, nsx_uuid): + try: + status_up = l2gwlib.get_gateway_device_status( + cluster, nsx_uuid) + if status_up: + return networkgw_db.STATUS_ACTIVE + else: + return networkgw_db.STATUS_DOWN + except api_exc.NsxApiException: + return networkgw_db.STATUS_UNKNOWN + except n_exc.NotFound: + return networkgw_db.ERROR + + +def get_nsx_device_statuses(cluster, tenant_id): + try: + status_dict = l2gwlib.get_gateway_devices_status( + cluster, tenant_id) + return dict((nsx_device_id, + networkgw_db.STATUS_ACTIVE if connected + else networkgw_db.STATUS_DOWN) for + (nsx_device_id, connected) in status_dict.iteritems()) + except api_exc.NsxApiException: + # Do not make a NSX API exception fatal + if tenant_id: + LOG.warn(_("Unable to retrieve operational status for gateway " + "devices belonging to tenant: %s"), tenant_id) + else: + LOG.warn(_("Unable to retrieve operational status for " + "gateway devices")) diff --git a/neutron/plugins/vmware/common/utils.py b/neutron/plugins/vmware/common/utils.py index 48773f63f0..67d719d7dd 100644 --- a/neutron/plugins/vmware/common/utils.py +++ b/neutron/plugins/vmware/common/utils.py @@ -27,6 +27,17 @@ MAX_DISPLAY_NAME_LEN = 40 NEUTRON_VERSION = version_info.release_string() +# Allowed network types for the NSX Plugin +class NetworkTypes: + """Allowed provider network types for the NSX Plugin.""" + L3_EXT = 'l3_ext' + STT = 'stt' + GRE = 'gre' + FLAT = 'flat' + VLAN = 'vlan' + BRIDGE = 'bridge' + + def get_tags(**kwargs): tags = ([dict(tag=value, scope=key) for key, value in kwargs.iteritems()]) diff --git a/neutron/plugins/vmware/dbexts/networkgw_db.py b/neutron/plugins/vmware/dbexts/networkgw_db.py index 58d14c5566..5609a0867f 100644 --- a/neutron/plugins/vmware/dbexts/networkgw_db.py +++ b/neutron/plugins/vmware/dbexts/networkgw_db.py @@ -11,7 +11,6 @@ # 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 sqlalchemy as sa @@ -37,6 +36,11 @@ SEGMENTATION_ID = 'segmentation_id' ALLOWED_CONNECTION_ATTRIBUTES = set((NETWORK_ID, SEGMENTATION_TYPE, SEGMENTATION_ID)) +# Constants for gateway device operational status +STATUS_UNKNOWN = "UNKNOWN" +STATUS_ERROR = "ERROR" +STATUS_ACTIVE = "ACTIVE" +STATUS_DOWN = "DOWN" class GatewayInUse(exceptions.InUse): @@ -48,6 +52,15 @@ class GatewayNotFound(exceptions.NotFound): message = _("Network Gateway %(gateway_id)s could not be found") +class GatewayDeviceInUse(exceptions.InUse): + message = _("Network Gateway Device '%(device_id)s' is still used by " + "one or more network gateways.") + + +class GatewayDeviceNotFound(exceptions.NotFound): + message = _("Network Gateway Device %(device_id)s could not be found.") + + class NetworkGatewayPortInUse(exceptions.InUse): message = _("Port '%(port_id)s' is owned by '%(device_owner)s' and " "therefore cannot be deleted directly via the port API.") @@ -104,7 +117,7 @@ class NetworkConnection(model_base.BASEV2, models_v2.HasTenant): primary_key=True) -class NetworkGatewayDevice(model_base.BASEV2): +class NetworkGatewayDeviceReference(model_base.BASEV2): id = sa.Column(sa.String(36), primary_key=True) network_gateway_id = sa.Column(sa.String(36), sa.ForeignKey('networkgateways.id', @@ -112,6 +125,20 @@ class NetworkGatewayDevice(model_base.BASEV2): interface_name = sa.Column(sa.String(64)) +class NetworkGatewayDevice(model_base.BASEV2, models_v2.HasId, + models_v2.HasTenant): + nsx_id = sa.Column(sa.String(36)) + # Optional name for the gateway device + name = sa.Column(sa.String(255)) + # Transport connector type. Not using enum as range of + # connector types might vary with backend version + connector_type = sa.Column(sa.String(10)) + # Transport connector IP Address + connector_ip = sa.Column(sa.String(64)) + # operational status + status = sa.Column(sa.String(16)) + + class NetworkGateway(model_base.BASEV2, models_v2.HasId, models_v2.HasTenant): """Defines the data model for a network gateway.""" @@ -119,7 +146,7 @@ class NetworkGateway(model_base.BASEV2, models_v2.HasId, # Tenant id is nullable for this resource tenant_id = sa.Column(sa.String(36)) default = sa.Column(sa.Boolean()) - devices = orm.relationship(NetworkGatewayDevice, + devices = orm.relationship(NetworkGatewayDeviceReference, backref='networkgateways', cascade='all,delete') network_connections = orm.relationship(NetworkConnection, lazy='joined') @@ -127,7 +154,8 @@ class NetworkGateway(model_base.BASEV2, models_v2.HasId, class NetworkGatewayMixin(networkgw.NetworkGatewayPluginBase): - resource = networkgw.RESOURCE_NAME.replace('-', '_') + gateway_resource = networkgw.GATEWAY_RESOURCE_NAME + device_resource = networkgw.DEVICE_RESOURCE_NAME def _get_network_gateway(self, context, gw_id): try: @@ -222,7 +250,7 @@ class NetworkGatewayMixin(networkgw.NetworkGatewayPluginBase): device_owner=port['device_owner']) def create_network_gateway(self, context, network_gateway): - gw_data = network_gateway[self.resource] + gw_data = network_gateway[self.gateway_resource] tenant_id = self._get_tenant_id_for_create(context, gw_data) with context.session.begin(subtransactions=True): gw_db = NetworkGateway( @@ -230,14 +258,17 @@ class NetworkGatewayMixin(networkgw.NetworkGatewayPluginBase): tenant_id=tenant_id, name=gw_data.get('name')) # Device list is guaranteed to be a valid list - gw_db.devices.extend([NetworkGatewayDevice(**device) + # TODO(salv-orlando): Enforce that gateway device identifiers + # in this list are among the tenant's NSX network gateway devices + # to avoid risk a tenant 'guessing' other tenant's network devices + gw_db.devices.extend([NetworkGatewayDeviceReference(**device) for device in gw_data['devices']]) context.session.add(gw_db) LOG.debug(_("Created network gateway with id:%s"), gw_db['id']) return self._make_network_gateway_dict(gw_db) def update_network_gateway(self, context, id, network_gateway): - gw_data = network_gateway[self.resource] + gw_data = network_gateway[self.gateway_resource] with context.session.begin(subtransactions=True): gw_db = self._get_network_gateway(context, id) if gw_db.default: @@ -363,9 +394,90 @@ class NetworkGatewayMixin(networkgw.NetworkGatewayPluginBase): raise MultipleGatewayConnections( gateway_id=network_gateway_id) # Remove gateway port from network - # FIXME(salvatore-orlando): Ensure state of port in NSX is + # FIXME(salvatore-orlando): Ensure state of port in NVP is # consistent with outcome of transaction self.delete_port(context, net_connection['port_id'], nw_gw_port_check=False) # Remove NetworkConnection record context.session.delete(net_connection) + + def _make_gateway_device_dict(self, gateway_device, fields=None, + include_nsx_id=False): + res = {'id': gateway_device['id'], + 'name': gateway_device['name'], + 'status': gateway_device['status'], + 'connector_type': gateway_device['connector_type'], + 'connector_ip': gateway_device['connector_ip'], + 'tenant_id': gateway_device['tenant_id']} + if include_nsx_id: + # Return the NSX mapping as well. This attribute will not be + # returned in the API response anyway. Ensure it will not be + # filtered out in field selection. + if fields: + fields.append('nsx_id') + res['nsx_id'] = gateway_device['nsx_id'] + return self._fields(res, fields) + + def _get_gateway_device(self, context, device_id): + try: + return self._get_by_id(context, NetworkGatewayDevice, device_id) + except sa_orm_exc.NoResultFound: + raise GatewayDeviceNotFound(device_id=device_id) + + def _is_device_in_use(self, context, device_id): + query = self._get_collection_query( + context, NetworkGatewayDeviceReference, {'id': [device_id]}) + return query.first() + + def get_gateway_device(self, context, device_id, fields=None, + include_nsx_id=False): + return self._make_gateway_device_dict( + self._get_gateway_device(context, device_id), + fields, include_nsx_id) + + def get_gateway_devices(self, context, filters=None, fields=None, + include_nsx_id=False): + query = self._get_collection_query(context, + NetworkGatewayDevice, + filters=filters) + return [self._make_gateway_device_dict(row, fields, include_nsx_id) + for row in query] + + def create_gateway_device(self, context, gateway_device, + initial_status=STATUS_UNKNOWN): + device_data = gateway_device[self.device_resource] + tenant_id = self._get_tenant_id_for_create(context, device_data) + with context.session.begin(subtransactions=True): + device_db = NetworkGatewayDevice( + id=device_data.get('id', uuidutils.generate_uuid()), + tenant_id=tenant_id, + name=device_data.get('name'), + connector_type=device_data['connector_type'], + connector_ip=device_data['connector_ip'], + status=initial_status) + context.session.add(device_db) + LOG.debug(_("Created network gateway device: %s"), device_db['id']) + return self._make_gateway_device_dict(device_db) + + def update_gateway_device(self, context, gateway_device_id, + gateway_device, include_nsx_id=False): + device_data = gateway_device[self.device_resource] + with context.session.begin(subtransactions=True): + device_db = self._get_gateway_device(context, gateway_device_id) + # Ensure there is something to update before doing it + if any([device_db[k] != device_data[k] for k in device_data]): + device_db.update(device_data) + LOG.debug(_("Updated network gateway device: %s"), + gateway_device_id) + return self._make_gateway_device_dict( + device_db, include_nsx_id=include_nsx_id) + + def delete_gateway_device(self, context, device_id): + with context.session.begin(subtransactions=True): + # A gateway device should not be deleted + # if it is used in any network gateway service + if self._is_device_in_use(context, device_id): + raise GatewayDeviceInUse(device_id=device_id) + device_db = self._get_gateway_device(context, device_id) + context.session.delete(device_db) + LOG.debug(_("Deleted network gateway device: %s."), device_id) diff --git a/neutron/plugins/vmware/extensions/networkgw.py b/neutron/plugins/vmware/extensions/networkgw.py index 5778335225..3d8ea8807a 100644 --- a/neutron/plugins/vmware/extensions/networkgw.py +++ b/neutron/plugins/vmware/extensions/networkgw.py @@ -19,24 +19,23 @@ from abc import abstractmethod from oslo.config import cfg -from neutron.api import extensions from neutron.api.v2 import attributes -from neutron.api.v2 import base -from neutron import manager -from neutron import quota +from neutron.api.v2 import resource_helper +from neutron.plugins.vmware.common.utils import NetworkTypes - -RESOURCE_NAME = "network_gateway" +GATEWAY_RESOURCE_NAME = "network_gateway" +DEVICE_RESOURCE_NAME = "gateway_device" # Use dash for alias and collection name -EXT_ALIAS = RESOURCE_NAME.replace('_', '-') -COLLECTION_NAME = "%ss" % EXT_ALIAS +EXT_ALIAS = GATEWAY_RESOURCE_NAME.replace('_', '-') +NETWORK_GATEWAYS = "%ss" % EXT_ALIAS +GATEWAY_DEVICES = "%ss" % DEVICE_RESOURCE_NAME.replace('_', '-') DEVICE_ID_ATTR = 'id' IFACE_NAME_ATTR = 'interface_name' # Attribute Map for Network Gateway Resource # TODO(salvatore-orlando): add admin state as other neutron resources RESOURCE_ATTRIBUTE_MAP = { - COLLECTION_NAME: { + NETWORK_GATEWAYS: { 'id': {'allow_post': False, 'allow_put': False, 'is_visible': True}, 'name': {'allow_post': True, 'allow_put': True, @@ -54,6 +53,28 @@ RESOURCE_ATTRIBUTE_MAP = { 'validate': {'type:string': None}, 'required_by_policy': True, 'is_visible': True} + }, + GATEWAY_DEVICES: { + 'id': {'allow_post': False, 'allow_put': False, + 'is_visible': True}, + 'name': {'allow_post': True, 'allow_put': True, + 'validate': {'type:string': None}, + 'is_visible': True, 'default': ''}, + 'client_certificate': {'allow_post': True, 'allow_put': True, + 'validate': {'type:string': None}, + 'is_visible': True}, + 'connector_type': {'allow_post': True, 'allow_put': True, + 'validate': {'type:connector_type': None}, + 'is_visible': True}, + 'connector_ip': {'allow_post': True, 'allow_put': True, + 'validate': {'type:ip_address': None}, + 'is_visible': True}, + 'tenant_id': {'allow_post': True, 'allow_put': False, + 'validate': {'type:string': None}, + 'required_by_policy': True, + 'is_visible': True}, + 'status': {'allow_post': False, 'allow_put': False, + 'is_visible': True}, } } @@ -85,6 +106,23 @@ def _validate_device_list(data, valid_values=None): return (_("%s: provided data are not iterable") % _validate_device_list.__name__) + +def _validate_connector_type(data, valid_values=None): + if not data: + # A connector type is compulsory + msg = _("A connector type is required to create a gateway device") + return msg + connector_types = (valid_values if valid_values else + [NetworkTypes.GRE, + NetworkTypes.STT, + NetworkTypes.BRIDGE, + 'ipsec%s' % NetworkTypes.GRE, + 'ipsec%s' % NetworkTypes.STT]) + if data not in connector_types: + msg = _("Unknown connector type: %s") % data + return msg + + nw_gw_quota_opts = [ cfg.IntOpt('quota_network_gateway', default=5, @@ -95,6 +133,7 @@ nw_gw_quota_opts = [ cfg.CONF.register_opts(nw_gw_quota_opts, 'QUOTAS') attributes.validators['type:device_list'] = _validate_device_list +attributes.validators['type:connector_type'] = _validate_connector_type class Networkgw(object): @@ -132,22 +171,21 @@ class Networkgw(object): @classmethod def get_resources(cls): """Returns Ext Resources.""" - plugin = manager.NeutronManager.get_plugin() - params = RESOURCE_ATTRIBUTE_MAP.get(COLLECTION_NAME, dict()) - member_actions = {'connect_network': 'PUT', - 'disconnect_network': 'PUT'} + member_actions = { + GATEWAY_RESOURCE_NAME.replace('_', '-'): { + 'connect_network': 'PUT', + 'disconnect_network': 'PUT'}} - # register quotas for network gateways - quota.QUOTAS.register_resource_by_name(RESOURCE_NAME) - collection_name = COLLECTION_NAME.replace('_', '-') - controller = base.create_resource(collection_name, - RESOURCE_NAME, - plugin, params, - member_actions=member_actions) - return [extensions.ResourceExtension(COLLECTION_NAME, - controller, - member_actions=member_actions)] + plural_mappings = resource_helper.build_plural_mappings( + {}, RESOURCE_ATTRIBUTE_MAP) + + return resource_helper.build_resource_info(plural_mappings, + RESOURCE_ATTRIBUTE_MAP, + None, + action_map=member_actions, + register_quota=True, + translate_name=True) def get_extended_resources(self, version): if version == "2.0": @@ -187,3 +225,23 @@ class NetworkGatewayPluginBase(object): def disconnect_network(self, context, network_gateway_id, network_mapping_info): pass + + @abstractmethod + def create_gateway_device(self, context, gateway_device): + pass + + @abstractmethod + def update_gateway_device(self, context, id, gateway_device): + pass + + @abstractmethod + def delete_gateway_device(self, context, id): + pass + + @abstractmethod + def get_gateway_device(self, context, id, fields=None): + pass + + @abstractmethod + def get_gateway_devices(self, context, filters=None, fields=None): + pass diff --git a/neutron/plugins/vmware/nsxlib/l2gateway.py b/neutron/plugins/vmware/nsxlib/l2gateway.py index 46128acbea..80397d51d7 100644 --- a/neutron/plugins/vmware/nsxlib/l2gateway.py +++ b/neutron/plugins/vmware/nsxlib/l2gateway.py @@ -29,6 +29,7 @@ HTTP_DELETE = "DELETE" HTTP_PUT = "PUT" GWSERVICE_RESOURCE = "gateway-service" +TRANSPORTNODE_RESOURCE = "transport-node" LOG = log.getLogger(__name__) @@ -58,7 +59,7 @@ def create_l2_gw_service(cluster, tenant_id, display_name, devices): "type": "L2GatewayServiceConfig" } return do_request( - "POST", _build_uri_path(GWSERVICE_RESOURCE), + HTTP_POST, _build_uri_path(GWSERVICE_RESOURCE), json.dumps(gwservice_obj), cluster=cluster) @@ -74,8 +75,8 @@ def plug_l2_gw_service(cluster, lswitch_id, lport_id, def get_l2_gw_service(cluster, gateway_id): return do_request( - "GET", _build_uri_path(GWSERVICE_RESOURCE, - resource_id=gateway_id), + HTTP_GET, _build_uri_path(GWSERVICE_RESOURCE, + resource_id=gateway_id), cluster=cluster) @@ -98,12 +99,101 @@ def update_l2_gw_service(cluster, gateway_id, display_name): # Nothing to update return gwservice_obj gwservice_obj["display_name"] = utils.check_and_truncate(display_name) - return do_request("PUT", _build_uri_path(GWSERVICE_RESOURCE, - resource_id=gateway_id), + return do_request(HTTP_PUT, _build_uri_path(GWSERVICE_RESOURCE, + resource_id=gateway_id), json.dumps(gwservice_obj), cluster=cluster) def delete_l2_gw_service(cluster, gateway_id): - do_request("DELETE", _build_uri_path(GWSERVICE_RESOURCE, - resource_id=gateway_id), + do_request(HTTP_DELETE, _build_uri_path(GWSERVICE_RESOURCE, + resource_id=gateway_id), cluster=cluster) + + +def _build_gateway_device_body(tenant_id, display_name, neutron_id, + connector_type, connector_ip, + client_certificate, tz_uuid): + + connector_type_mappings = { + utils.NetworkTypes.STT: "STTConnector", + utils.NetworkTypes.GRE: "GREConnector", + utils.NetworkTypes.BRIDGE: "BridgeConnector", + 'ipsec%s' % utils.NetworkTypes.STT: "IPsecSTT", + 'ipsec%s' % utils.NetworkTypes.GRE: "IPsecGRE"} + nsx_connector_type = connector_type_mappings[connector_type] + body = {"display_name": utils.check_and_truncate(display_name), + "tags": utils.get_tags(os_tid=tenant_id, + q_gw_dev_id=neutron_id), + "transport_connectors": [ + {"transport_zone_uuid": tz_uuid, + "ip_address": connector_ip, + "type": nsx_connector_type}], + "admin_status_enabled": True} + if client_certificate: + body["credential"] = {"client_certificate": + {"pem_encoded": client_certificate}, + "type": "SecurityCertificateCredential"} + return body + + +def create_gateway_device(cluster, tenant_id, display_name, neutron_id, + tz_uuid, connector_type, connector_ip, + client_certificate): + body = _build_gateway_device_body(tenant_id, display_name, neutron_id, + connector_type, connector_ip, + client_certificate, tz_uuid) + return do_request( + HTTP_POST, _build_uri_path(TRANSPORTNODE_RESOURCE), + json.dumps(body), cluster=cluster) + + +def update_gateway_device(cluster, gateway_id, tenant_id, + display_name, neutron_id, + tz_uuid, connector_type, connector_ip, + client_certificate): + body = _build_gateway_device_body(tenant_id, display_name, neutron_id, + connector_type, connector_ip, + client_certificate, tz_uuid) + return do_request( + HTTP_PUT, + _build_uri_path(TRANSPORTNODE_RESOURCE, resource_id=gateway_id), + json.dumps(body), cluster=cluster) + + +def delete_gateway_device(cluster, device_uuid): + return do_request(HTTP_DELETE, + _build_uri_path(TRANSPORTNODE_RESOURCE, + device_uuid), + cluster=cluster) + + +def get_gateway_device_status(cluster, device_uuid): + status_res = do_request(HTTP_GET, + _build_uri_path(TRANSPORTNODE_RESOURCE, + device_uuid, + extra_action='status'), + cluster=cluster) + # Returns the connection status + return status_res['connection']['connected'] + + +def get_gateway_devices_status(cluster, tenant_id=None): + if tenant_id: + gw_device_query_path = _build_uri_path( + TRANSPORTNODE_RESOURCE, + fields="uuid,tags", + relations="TransportNodeStatus", + filters={'tag': tenant_id, + 'tag_scope': 'os_tid'}) + else: + gw_device_query_path = _build_uri_path( + TRANSPORTNODE_RESOURCE, + fields="uuid,tags", + relations="TransportNodeStatus") + + response = get_all_query_pages(gw_device_query_path, cluster) + results = {} + for item in response: + results[item['uuid']] = (item['_relations']['TransportNodeStatus'] + ['connection']['connected']) + return results diff --git a/neutron/plugins/vmware/plugins/base.py b/neutron/plugins/vmware/plugins/base.py index 2b832d1324..886938ac2e 100644 --- a/neutron/plugins/vmware/plugins/base.py +++ b/neutron/plugins/vmware/plugins/base.py @@ -60,6 +60,7 @@ from neutron.plugins.vmware.common import exceptions as nsx_exc from neutron.plugins.vmware.common import nsx_utils from neutron.plugins.vmware.common import securitygroups as sg_utils from neutron.plugins.vmware.common import sync +from neutron.plugins.vmware.common.utils import NetworkTypes from neutron.plugins.vmware.dbexts import db as nsx_db from neutron.plugins.vmware.dbexts import distributedrouter as dist_rtr from neutron.plugins.vmware.dbexts import maclearning as mac_db @@ -83,17 +84,6 @@ NSX_EXTGW_NAT_RULES_ORDER = 255 NSX_DEFAULT_NEXTHOP = '1.1.1.1' -# Provider network extension - allowed network types for the NSX Plugin -class NetworkTypes: - """Allowed provider network types for the NSX Plugin.""" - L3_EXT = 'l3_ext' - STT = 'stt' - GRE = 'gre' - FLAT = 'flat' - VLAN = 'vlan' - BRIDGE = 'bridge' - - class NsxPluginV2(addr_pair_db.AllowedAddressPairsMixin, agentschedulers_db.DhcpAgentSchedulerDbMixin, db_base_plugin_v2.NeutronDbPluginV2, @@ -205,7 +195,7 @@ class NsxPluginV2(addr_pair_db.AllowedAddressPairsMixin, def_gw_data = {'id': def_l2_gw_uuid, 'name': 'default L2 gateway service', 'devices': []} - gw_res_name = networkgw.RESOURCE_NAME.replace('-', '_') + gw_res_name = networkgw.GATEWAY_RESOURCE_NAME.replace('-', '_') def_network_gw = super( NsxPluginV2, self).create_network_gateway( ctx, {gw_res_name: def_gw_data}) @@ -2009,7 +1999,7 @@ class NsxPluginV2(addr_pair_db.AllowedAddressPairsMixin, # Ensure the default gateway in the config file is in sync with the db self._ensure_default_network_gateway() # Need to re-do authZ checks here in order to avoid creation on NSX - gw_data = network_gateway[networkgw.RESOURCE_NAME.replace('-', '_')] + gw_data = network_gateway[networkgw.GATEWAY_RESOURCE_NAME] tenant_id = self._get_tenant_id_for_create(context, gw_data) devices = gw_data['devices'] # Populate default physical network where not specified @@ -2017,8 +2007,15 @@ class NsxPluginV2(addr_pair_db.AllowedAddressPairsMixin, if not device.get('interface_name'): device['interface_name'] = self.cluster.default_interface_name try: + # Replace Neutron device identifiers with NSX identifiers + # TODO(salv-orlando): Make this operation more efficient doing a + # single DB query for all devices + nsx_devices = [{'id': self._get_nsx_device_id(context, + device['id']), + 'interface_name': device['interface_name']} for + device in devices] nsx_res = l2gwlib.create_l2_gw_service( - self.cluster, tenant_id, gw_data['name'], devices) + self.cluster, tenant_id, gw_data['name'], nsx_devices) nsx_uuid = nsx_res.get('uuid') except api_exc.Conflict: raise nsx_exc.L2GatewayAlreadyInUse(gateway=gw_data['name']) @@ -2027,8 +2024,8 @@ class NsxPluginV2(addr_pair_db.AllowedAddressPairsMixin, LOG.exception(err_msg) raise nsx_exc.NsxPluginException(err_msg=err_msg) gw_data['id'] = nsx_uuid - return super(NsxPluginV2, self).create_network_gateway(context, - network_gateway) + return super(NsxPluginV2, self).create_network_gateway( + context, network_gateway) def delete_network_gateway(self, context, gateway_id): """Remove a layer-2 network gateway. @@ -2069,7 +2066,7 @@ class NsxPluginV2(addr_pair_db.AllowedAddressPairsMixin, # Ensure the default gateway in the config file is in sync with the db self._ensure_default_network_gateway() # Update gateway on backend when there's a name change - name = network_gateway[networkgw.RESOURCE_NAME].get('name') + name = network_gateway[networkgw.GATEWAY_RESOURCE_NAME].get('name') if name: try: l2gwlib.update_l2_gw_service(self.cluster, id, name) @@ -2098,6 +2095,205 @@ class NsxPluginV2(addr_pair_db.AllowedAddressPairsMixin, return super(NsxPluginV2, self).disconnect_network( context, network_gateway_id, network_mapping_info) + def _get_nsx_device_id(self, context, device_id): + return self._get_gateway_device(context, device_id)['nsx_id'] + + # TODO(salv-orlando): Handlers for Gateway device operations should be + # moved into the appropriate nsx_handlers package once the code for the + # blueprint nsx-async-backend-communication merges + def create_gateway_device_handler(self, context, gateway_device, + client_certificate): + neutron_id = gateway_device['id'] + try: + nsx_res = l2gwlib.create_gateway_device( + self.cluster, + gateway_device['tenant_id'], + gateway_device['name'], + neutron_id, + self.cluster.default_tz_uuid, + gateway_device['connector_type'], + gateway_device['connector_ip'], + client_certificate) + + # Fetch status (it needs another NSX API call) + device_status = nsx_utils.get_nsx_device_status(self.cluster, + nsx_res['uuid']) + + # set NSX GW device in neutron database and update status + with context.session.begin(subtransactions=True): + query = self._model_query( + context, networkgw_db.NetworkGatewayDevice).filter( + networkgw_db.NetworkGatewayDevice.id == neutron_id) + query.update({'status': device_status, + 'nsx_id': nsx_res['uuid']}, + synchronize_session=False) + LOG.debug(_("Neutron gateway device: %(neutron_id)s; " + "NSX transport node identifier: %(nsx_id)s; " + "Operational status: %(status)s."), + {'neutron_id': neutron_id, + 'nsx_id': nsx_res['uuid'], + 'status': device_status}) + return device_status + except api_exc.NsxApiException: + # Remove gateway device from neutron database + with excutils.save_and_reraise_exception(): + LOG.exception(_("Unable to create gateway device: %s on NSX " + "backend."), neutron_id) + with context.session.begin(subtransactions=True): + query = self._model_query( + context, networkgw_db.NetworkGatewayDevice).filter( + networkgw_db.NetworkGatewayDevice.id == neutron_id) + query.delete(synchronize_session=False) + + def update_gateway_device_handler(self, context, gateway_device, + old_gateway_device_data, + client_certificate): + nsx_id = gateway_device['nsx_id'] + neutron_id = gateway_device['id'] + try: + l2gwlib.update_gateway_device( + self.cluster, + nsx_id, + gateway_device['tenant_id'], + gateway_device['name'], + neutron_id, + self.cluster.default_tz_uuid, + gateway_device['connector_type'], + gateway_device['connector_ip'], + client_certificate) + + # Fetch status (it needs another NSX API call) + device_status = nsx_utils.get_nsx_device_status(self.cluster, + nsx_id) + + # update status + with context.session.begin(subtransactions=True): + query = self._model_query( + context, networkgw_db.NetworkGatewayDevice).filter( + networkgw_db.NetworkGatewayDevice.id == neutron_id) + query.update({'status': device_status}, + synchronize_session=False) + LOG.debug(_("Neutron gateway device: %(neutron_id)s; " + "NSX transport node identifier: %(nsx_id)s; " + "Operational status: %(status)s."), + {'neutron_id': neutron_id, + 'nsx_id': nsx_id, + 'status': device_status}) + return device_status + except api_exc.NsxApiException: + # Rollback gateway device on neutron database + # As the NSX failure could be transient, we don't put the + # gateway device in error status here. + with excutils.save_and_reraise_exception(): + LOG.exception(_("Unable to update gateway device: %s on NSX " + "backend."), neutron_id) + super(NsxPluginV2, self).update_gateway_device( + context, neutron_id, old_gateway_device_data) + except n_exc.NotFound: + # The gateway device was probably deleted in the backend. + # The DB change should be rolled back and the status must + # be put in error + with excutils.save_and_reraise_exception(): + LOG.exception(_("Unable to update gateway device: %s on NSX " + "backend, as the gateway was not found on " + "the NSX backend."), neutron_id) + with context.session.begin(subtransactions=True): + super(NsxPluginV2, self).update_gateway_device( + context, neutron_id, old_gateway_device_data) + query = self._model_query( + context, networkgw_db.NetworkGatewayDevice).filter( + networkgw_db.NetworkGatewayDevice.id == neutron_id) + query.update({'status': networkgw_db.ERROR}, + synchronize_session=False) + + def get_gateway_device(self, context, device_id, fields=None): + # Get device from database + gw_device = super(NsxPluginV2, self).get_gateway_device( + context, device_id, fields, include_nsx_id=True) + # Fetch status from NSX + nsx_id = gw_device['nsx_id'] + device_status = nsx_utils.get_nsx_device_status(self.cluster, nsx_id) + # TODO(salv-orlando): Asynchronous sync for gateway device status + # Update status in database + with context.session.begin(subtransactions=True): + query = self._model_query( + context, networkgw_db.NetworkGatewayDevice).filter( + networkgw_db.NetworkGatewayDevice.id == device_id) + query.update({'status': device_status}, + synchronize_session=False) + gw_device['status'] = device_status + return gw_device + + def get_gateway_devices(self, context, filters=None, fields=None): + # Get devices from database + devices = super(NsxPluginV2, self).get_gateway_devices( + context, filters, fields, include_nsx_id=True) + # Fetch operational status from NVP, filter by tenant tag + # TODO(salv-orlando): Asynchronous sync for gateway device status + tenant_id = context.tenant_id if not context.is_admin else None + nsx_statuses = nsx_utils.get_nsx_device_statuses(self.cluster, + tenant_id) + # Update statuses in database + with context.session.begin(subtransactions=True): + for device in devices: + new_status = nsx_statuses.get(device['nsx_id']) + if new_status: + device['status'] = new_status + return devices + + def create_gateway_device(self, context, gateway_device): + # NOTE(salv-orlando): client-certificate will not be stored + # in the database + device_data = gateway_device[networkgw.DEVICE_RESOURCE_NAME] + client_certificate = device_data.pop('client_certificate') + gw_device = super(NsxPluginV2, self).create_gateway_device( + context, gateway_device) + # DB operation was successful, perform NSX operation + gw_device['status'] = self.create_gateway_device_handler( + context, gw_device, client_certificate) + return gw_device + + def update_gateway_device(self, context, device_id, + gateway_device): + # NOTE(salv-orlando): client-certificate will not be stored + # in the database + client_certificate = ( + gateway_device[networkgw.DEVICE_RESOURCE_NAME].pop( + 'client_certificate', None)) + # Retrive current state from DB in case a rollback should be needed + old_gw_device_data = super(NsxPluginV2, self).get_gateway_device( + context, device_id, include_nsx_id=True) + gw_device = super(NsxPluginV2, self).update_gateway_device( + context, device_id, gateway_device, include_nsx_id=True) + # DB operation was successful, perform NSX operation + gw_device['status'] = self.update_gateway_device_handler( + context, gw_device, old_gw_device_data, client_certificate) + gw_device.pop('nsx_id') + return gw_device + + def delete_gateway_device(self, context, device_id): + nsx_device_id = self._get_nsx_device_id(context, device_id) + super(NsxPluginV2, self).delete_gateway_device( + context, device_id) + # DB operation was successful, peform NSX operation + # TODO(salv-orlando): State consistency with neutron DB + # should be ensured even in case of backend failures + try: + l2gwlib.delete_gateway_device(self.cluster, nsx_device_id) + except n_exc.NotFound: + LOG.warn(_("Removal of gateway device: %(neutron_id)s failed on " + "NSX backend (NSX id:%(nsx_id)s) because the NSX " + "resource was not found"), + {'neutron_id': device_id, 'nsx_id': nsx_device_id}) + except api_exc.NsxApiException: + LOG.exception(_("Removal of gateway device: %(neutron_id)s " + "failed on NSX backend (NSX id:%(nsx_id)s). " + "Neutron and NSX states have diverged."), + {'neutron_id': device_id, + 'nsx_id': nsx_device_id}) + # In this case a 500 should be returned + raise + def create_security_group(self, context, security_group, default_sg=False): """Create security group. diff --git a/neutron/tests/unit/vmware/extensions/test_networkgw.py b/neutron/tests/unit/vmware/extensions/test_networkgw.py index 25c542f57a..a0b26101b5 100644 --- a/neutron/tests/unit/vmware/extensions/test_networkgw.py +++ b/neutron/tests/unit/vmware/extensions/test_networkgw.py @@ -27,11 +27,11 @@ from neutron import context from neutron.db import api as db_api from neutron.db import db_base_plugin_v2 from neutron import manager -from neutron.openstack.common import uuidutils from neutron.plugins.vmware.api_client import exception as api_exc from neutron.plugins.vmware.dbexts import networkgw_db from neutron.plugins.vmware.extensions import networkgw from neutron.plugins.vmware import nsxlib +from neutron.plugins.vmware.nsxlib import l2gateway as l2gwlib from neutron import quota from neutron.tests import base from neutron.tests.unit import test_api_v2 @@ -69,7 +69,8 @@ class NetworkGatewayExtensionTestCase(base.BaseTestCase): super(NetworkGatewayExtensionTestCase, self).setUp() plugin = '%s.%s' % (networkgw.__name__, networkgw.NetworkGatewayPluginBase.__name__) - self._resource = networkgw.RESOURCE_NAME.replace('-', '_') + self._gw_resource = networkgw.GATEWAY_RESOURCE_NAME + self._dev_resource = networkgw.DEVICE_RESOURCE_NAME # Ensure existing ExtensionManager is not used extensions.PluginAwareExtensionManager._instance = None @@ -100,67 +101,67 @@ class NetworkGatewayExtensionTestCase(base.BaseTestCase): def test_network_gateway_create(self): nw_gw_id = _uuid() - data = {self._resource: {'name': 'nw-gw', - 'tenant_id': _uuid(), - 'devices': [{'id': _uuid(), - 'interface_name': 'xxx'}]}} - return_value = data[self._resource].copy() + data = {self._gw_resource: {'name': 'nw-gw', + 'tenant_id': _uuid(), + 'devices': [{'id': _uuid(), + 'interface_name': 'xxx'}]}} + return_value = data[self._gw_resource].copy() return_value.update({'id': nw_gw_id}) instance = self.plugin.return_value instance.create_network_gateway.return_value = return_value - res = self.api.post_json(_get_path(networkgw.COLLECTION_NAME), data) + res = self.api.post_json(_get_path(networkgw.NETWORK_GATEWAYS), data) instance.create_network_gateway.assert_called_with( mock.ANY, network_gateway=data) self.assertEqual(res.status_int, exc.HTTPCreated.code) - self.assertIn(self._resource, res.json) - nw_gw = res.json[self._resource] + self.assertIn(self._gw_resource, res.json) + nw_gw = res.json[self._gw_resource] self.assertEqual(nw_gw['id'], nw_gw_id) def _test_network_gateway_create_with_error( self, data, error_code=exc.HTTPBadRequest.code): - res = self.api.post_json(_get_path(networkgw.COLLECTION_NAME), data, + res = self.api.post_json(_get_path(networkgw.NETWORK_GATEWAYS), data, expect_errors=True) self.assertEqual(res.status_int, error_code) def test_network_gateway_create_invalid_device_spec(self): - data = {self._resource: {'name': 'nw-gw', - 'tenant_id': _uuid(), - 'devices': [{'id': _uuid(), - 'invalid': 'xxx'}]}} + data = {self._gw_resource: {'name': 'nw-gw', + 'tenant_id': _uuid(), + 'devices': [{'id': _uuid(), + 'invalid': 'xxx'}]}} self._test_network_gateway_create_with_error(data) def test_network_gateway_create_extra_attr_in_device_spec(self): - data = {self._resource: {'name': 'nw-gw', - 'tenant_id': _uuid(), - 'devices': [{'id': _uuid(), - 'interface_name': 'xxx', - 'extra_attr': 'onetoomany'}]}} + data = {self._gw_resource: {'name': 'nw-gw', + 'tenant_id': _uuid(), + 'devices': + [{'id': _uuid(), + 'interface_name': 'xxx', + 'extra_attr': 'onetoomany'}]}} self._test_network_gateway_create_with_error(data) def test_network_gateway_update(self): nw_gw_name = 'updated' - data = {self._resource: {'name': nw_gw_name}} + data = {self._gw_resource: {'name': nw_gw_name}} nw_gw_id = _uuid() return_value = {'id': nw_gw_id, 'name': nw_gw_name} instance = self.plugin.return_value instance.update_network_gateway.return_value = return_value - res = self.api.put_json(_get_path('%s/%s' % (networkgw.COLLECTION_NAME, - nw_gw_id)), - data) + res = self.api.put_json( + _get_path('%s/%s' % (networkgw.NETWORK_GATEWAYS, nw_gw_id)), data) instance.update_network_gateway.assert_called_with( mock.ANY, nw_gw_id, network_gateway=data) self.assertEqual(res.status_int, exc.HTTPOk.code) - self.assertIn(self._resource, res.json) - nw_gw = res.json[self._resource] + self.assertIn(self._gw_resource, res.json) + nw_gw = res.json[self._gw_resource] self.assertEqual(nw_gw['id'], nw_gw_id) self.assertEqual(nw_gw['name'], nw_gw_name) def test_network_gateway_delete(self): nw_gw_id = _uuid() instance = self.plugin.return_value - res = self.api.delete(_get_path('%s/%s' % (networkgw.COLLECTION_NAME, + res = self.api.delete(_get_path('%s/%s' % (networkgw.NETWORK_GATEWAYS, nw_gw_id))) instance.delete_network_gateway.assert_called_with(mock.ANY, @@ -169,15 +170,15 @@ class NetworkGatewayExtensionTestCase(base.BaseTestCase): def test_network_gateway_get(self): nw_gw_id = _uuid() - return_value = {self._resource: {'name': 'test', - 'devices': - [{'id': _uuid(), - 'interface_name': 'xxx'}], - 'id': nw_gw_id}} + return_value = {self._gw_resource: {'name': 'test', + 'devices': + [{'id': _uuid(), + 'interface_name': 'xxx'}], + 'id': nw_gw_id}} instance = self.plugin.return_value instance.get_network_gateway.return_value = return_value - res = self.api.get(_get_path('%s/%s' % (networkgw.COLLECTION_NAME, + res = self.api.get(_get_path('%s/%s' % (networkgw.NETWORK_GATEWAYS, nw_gw_id))) instance.get_network_gateway.assert_called_with(mock.ANY, @@ -187,15 +188,15 @@ class NetworkGatewayExtensionTestCase(base.BaseTestCase): def test_network_gateway_list(self): nw_gw_id = _uuid() - return_value = [{self._resource: {'name': 'test', - 'devices': - [{'id': _uuid(), - 'interface_name': 'xxx'}], - 'id': nw_gw_id}}] + return_value = [{self._gw_resource: {'name': 'test', + 'devices': + [{'id': _uuid(), + 'interface_name': 'xxx'}], + 'id': nw_gw_id}}] instance = self.plugin.return_value instance.get_network_gateways.return_value = return_value - res = self.api.get(_get_path(networkgw.COLLECTION_NAME)) + res = self.api.get(_get_path(networkgw.NETWORK_GATEWAYS)) instance.get_network_gateways.assert_called_with(mock.ANY, fields=mock.ANY, @@ -216,7 +217,7 @@ class NetworkGatewayExtensionTestCase(base.BaseTestCase): instance = self.plugin.return_value instance.connect_network.return_value = return_value res = self.api.put_json(_get_path('%s/%s/connect_network' % - (networkgw.COLLECTION_NAME, + (networkgw.NETWORK_GATEWAYS, nw_gw_id)), mapping_data) instance.connect_network.assert_called_with(mock.ANY, @@ -233,7 +234,7 @@ class NetworkGatewayExtensionTestCase(base.BaseTestCase): mapping_data = {'network_id': nw_id} instance = self.plugin.return_value res = self.api.put_json(_get_path('%s/%s/disconnect_network' % - (networkgw.COLLECTION_NAME, + (networkgw.NETWORK_GATEWAYS, nw_gw_id)), mapping_data) instance.disconnect_network.assert_called_with(mock.ANY, @@ -241,6 +242,116 @@ class NetworkGatewayExtensionTestCase(base.BaseTestCase): mapping_data) self.assertEqual(res.status_int, exc.HTTPOk.code) + def test_gateway_device_get(self): + gw_dev_id = _uuid() + return_value = {self._dev_resource: {'name': 'test', + 'connector_type': 'stt', + 'connector_ip': '1.1.1.1', + 'id': gw_dev_id}} + instance = self.plugin.return_value + instance.get_gateway_device.return_value = return_value + + res = self.api.get(_get_path('%s/%s' % (networkgw.GATEWAY_DEVICES, + gw_dev_id))) + + instance.get_gateway_device.assert_called_with(mock.ANY, + gw_dev_id, + fields=mock.ANY) + self.assertEqual(res.status_int, exc.HTTPOk.code) + + def test_gateway_device_list(self): + gw_dev_id = _uuid() + return_value = [{self._dev_resource: {'name': 'test', + 'connector_type': 'stt', + 'connector_ip': '1.1.1.1', + 'id': gw_dev_id}}] + instance = self.plugin.return_value + instance.get_gateway_devices.return_value = return_value + + res = self.api.get(_get_path(networkgw.GATEWAY_DEVICES)) + + instance.get_gateway_devices.assert_called_with(mock.ANY, + fields=mock.ANY, + filters=mock.ANY) + self.assertEqual(res.status_int, exc.HTTPOk.code) + + def test_gateway_device_create(self): + gw_dev_id = _uuid() + data = {self._dev_resource: {'name': 'test-dev', + 'tenant_id': _uuid(), + 'client_certificate': 'xyz', + 'connector_type': 'stt', + 'connector_ip': '1.1.1.1'}} + return_value = data[self._dev_resource].copy() + return_value.update({'id': gw_dev_id}) + instance = self.plugin.return_value + instance.create_gateway_device.return_value = return_value + res = self.api.post_json(_get_path(networkgw.GATEWAY_DEVICES), data) + instance.create_gateway_device.assert_called_with( + mock.ANY, gateway_device=data) + self.assertEqual(res.status_int, exc.HTTPCreated.code) + self.assertIn(self._dev_resource, res.json) + gw_dev = res.json[self._dev_resource] + self.assertEqual(gw_dev['id'], gw_dev_id) + + def _test_gateway_device_create_with_error( + self, data, error_code=exc.HTTPBadRequest.code): + res = self.api.post_json(_get_path(networkgw.GATEWAY_DEVICES), data, + expect_errors=True) + self.assertEqual(res.status_int, error_code) + + def test_gateway_device_create_invalid_connector_type(self): + data = {self._gw_resource: {'name': 'test-dev', + 'client_certificate': 'xyz', + 'tenant_id': _uuid(), + 'connector_type': 'invalid', + 'connector_ip': '1.1.1.1'}} + self._test_gateway_device_create_with_error(data) + + def test_gateway_device_create_invalid_connector_ip(self): + data = {self._gw_resource: {'name': 'test-dev', + 'client_certificate': 'xyz', + 'tenant_id': _uuid(), + 'connector_type': 'stt', + 'connector_ip': 'invalid'}} + self._test_gateway_device_create_with_error(data) + + def test_gateway_device_create_extra_attr_in_device_spec(self): + data = {self._gw_resource: {'name': 'test-dev', + 'client_certificate': 'xyz', + 'tenant_id': _uuid(), + 'alien_attribute': 'E.T.', + 'connector_type': 'stt', + 'connector_ip': '1.1.1.1'}} + self._test_gateway_device_create_with_error(data) + + def test_gateway_device_update(self): + gw_dev_name = 'updated' + data = {self._dev_resource: {'name': gw_dev_name}} + gw_dev_id = _uuid() + return_value = {'id': gw_dev_id, + 'name': gw_dev_name} + + instance = self.plugin.return_value + instance.update_gateway_device.return_value = return_value + res = self.api.put_json( + _get_path('%s/%s' % (networkgw.GATEWAY_DEVICES, gw_dev_id)), data) + instance.update_gateway_device.assert_called_with( + mock.ANY, gw_dev_id, gateway_device=data) + self.assertEqual(res.status_int, exc.HTTPOk.code) + self.assertIn(self._dev_resource, res.json) + gw_dev = res.json[self._dev_resource] + self.assertEqual(gw_dev['id'], gw_dev_id) + self.assertEqual(gw_dev['name'], gw_dev_name) + + def test_gateway_device_delete(self): + gw_dev_id = _uuid() + instance = self.plugin.return_value + res = self.api.delete(_get_path('%s/%s' % (networkgw.GATEWAY_DEVICES, + gw_dev_id))) + instance.delete_gateway_device.assert_called_with(mock.ANY, gw_dev_id) + self.assertEqual(res.status_int, exc.HTTPNoContent.code) + class NetworkGatewayDbTestCase(test_db_plugin.NeutronDbPluginV2TestCase): """Unit tests for Network Gateway DB support.""" @@ -250,21 +361,23 @@ class NetworkGatewayDbTestCase(test_db_plugin.NeutronDbPluginV2TestCase): plugin = '%s.%s' % (__name__, TestNetworkGatewayPlugin.__name__) if not ext_mgr: ext_mgr = TestExtensionManager() - self.resource = networkgw.RESOURCE_NAME.replace('-', '_') + self.gw_resource = networkgw.GATEWAY_RESOURCE_NAME + self.dev_resource = networkgw.DEVICE_RESOURCE_NAME + super(NetworkGatewayDbTestCase, self).setUp(plugin=plugin, ext_mgr=ext_mgr) def _create_network_gateway(self, fmt, tenant_id, name=None, devices=None, arg_list=None, **kwargs): - data = {self.resource: {'tenant_id': tenant_id, - 'devices': devices}} + data = {self.gw_resource: {'tenant_id': tenant_id, + 'devices': devices}} if name: - data[self.resource]['name'] = name + data[self.gw_resource]['name'] = name for arg in arg_list or (): # Arg must be present and not empty if arg in kwargs and kwargs[arg]: - data[self.resource][arg] = kwargs[arg] - nw_gw_req = self.new_create_request(networkgw.COLLECTION_NAME, + data[self.gw_resource][arg] = kwargs[arg] + nw_gw_req = self.new_create_request(networkgw.NETWORK_GATEWAYS, data, fmt) if (kwargs.get('set_context') and tenant_id): # create a specific auth context for this request @@ -275,16 +388,89 @@ class NetworkGatewayDbTestCase(test_db_plugin.NeutronDbPluginV2TestCase): @contextlib.contextmanager def _network_gateway(self, name='gw1', devices=None, fmt='json', tenant_id=_uuid()): + device = None if not devices: - devices = [{'id': _uuid(), 'interface_name': 'xyz'}] + device_res = self._create_gateway_device( + fmt, tenant_id, 'stt', '1.1.1.1', 'xxxxxx', + name='whatever') + if device_res.status_int >= 400: + raise exc.HTTPClientError(code=device_res.status_int) + device = self.deserialize(fmt, device_res) + devices = [{'id': device[self.dev_resource]['id'], + 'interface_name': 'xyz'}] + res = self._create_network_gateway(fmt, tenant_id, name=name, devices=devices) - network_gateway = self.deserialize(fmt, res) if res.status_int >= 400: raise exc.HTTPClientError(code=res.status_int) + network_gateway = self.deserialize(fmt, res) yield network_gateway - self._delete(networkgw.COLLECTION_NAME, - network_gateway[self.resource]['id']) + + self._delete(networkgw.NETWORK_GATEWAYS, + network_gateway[self.gw_resource]['id']) + if device: + self._delete(networkgw.GATEWAY_DEVICES, + device[self.dev_resource]['id']) + + def _create_gateway_device(self, fmt, tenant_id, + connector_type, connector_ip, + client_certificate, name=None, + set_context=False): + data = {self.dev_resource: {'tenant_id': tenant_id, + 'connector_type': connector_type, + 'connector_ip': connector_ip, + 'client_certificate': client_certificate}} + if name: + data[self.dev_resource]['name'] = name + gw_dev_req = self.new_create_request(networkgw.GATEWAY_DEVICES, + data, fmt) + if (set_context and tenant_id): + # create a specific auth context for this request + gw_dev_req.environ['neutron.context'] = context.Context( + '', tenant_id) + return gw_dev_req.get_response(self.ext_api) + + def _update_gateway_device(self, fmt, gateway_device_id, + connector_type=None, connector_ip=None, + client_certificate=None, name=None, + set_context=False, tenant_id=None): + data = {self.dev_resource: {}} + if connector_type: + data[self.dev_resource]['connector_type'] = connector_type + if connector_ip: + data[self.dev_resource]['connector_ip'] = connector_ip + if client_certificate: + data[self.dev_resource]['client_certificate'] = client_certificate + if name: + data[self.dev_resource]['name'] = name + gw_dev_req = self.new_update_request(networkgw.GATEWAY_DEVICES, + data, gateway_device_id, fmt) + if (set_context and tenant_id): + # create a specific auth context for this request + gw_dev_req.environ['neutron.context'] = context.Context( + '', tenant_id) + return gw_dev_req.get_response(self.ext_api) + + @contextlib.contextmanager + def _gateway_device(self, name='gw_dev', + connector_type='stt', + connector_ip='1.1.1.1', + client_certificate='xxxxxxxxxxxxxxx', + fmt='json', tenant_id=_uuid()): + res = self._create_gateway_device( + fmt, + tenant_id, + connector_type=connector_type, + connector_ip=connector_ip, + client_certificate=client_certificate, + name=name) + if res.status_int >= 400: + raise exc.HTTPClientError(code=res.status_int) + gateway_device = self.deserialize(fmt, res) + yield gateway_device + + self._delete(networkgw.GATEWAY_DEVICES, + gateway_device[self.dev_resource]['id']) def _gateway_action(self, action, network_gateway_id, network_id, segmentation_type, segmentation_id=None, @@ -294,7 +480,7 @@ class NetworkGatewayDbTestCase(test_db_plugin.NeutronDbPluginV2TestCase): if segmentation_id: connection_data['segmentation_id'] = segmentation_id - req = self.new_action_request(networkgw.COLLECTION_NAME, + req = self.new_action_request(networkgw.NETWORK_GATEWAYS, connection_data, network_gateway_id, "%s_network" % action) @@ -307,7 +493,7 @@ class NetworkGatewayDbTestCase(test_db_plugin.NeutronDbPluginV2TestCase): with self._network_gateway() as gw: with self.network() as net: body = self._gateway_action('connect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net['network']['id'], segmentation_type, segmentation_id) @@ -320,10 +506,10 @@ class NetworkGatewayDbTestCase(test_db_plugin.NeutronDbPluginV2TestCase): gw_port_id = connection_info['port_id'] port_body = self._show('ports', gw_port_id) self.assertEqual(port_body['port']['device_id'], - gw[self.resource]['id']) + gw[self.gw_resource]['id']) # Clean up - otherwise delete will fail body = self._gateway_action('disconnect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net['network']['id'], segmentation_type, segmentation_id) @@ -332,90 +518,98 @@ class NetworkGatewayDbTestCase(test_db_plugin.NeutronDbPluginV2TestCase): expected_code=exc.HTTPNotFound.code) def test_create_network_gateway(self): - name = 'test-gw' - devices = [{'id': _uuid(), 'interface_name': 'xxx'}, - {'id': _uuid(), 'interface_name': 'yyy'}] - keys = [('devices', devices), ('name', name)] - with self._network_gateway(name=name, devices=devices) as gw: - for k, v in keys: - self.assertEqual(gw[self.resource][k], v) + with contextlib.nested( + self._gateway_device(name='dev_1'), + self._gateway_device(name='dev_2')) as (dev_1, dev_2): + name = 'test-gw' + dev_1_id = dev_1[self.dev_resource]['id'] + dev_2_id = dev_2[self.dev_resource]['id'] + devices = [{'id': dev_1_id, 'interface_name': 'xxx'}, + {'id': dev_2_id, 'interface_name': 'yyy'}] + keys = [('devices', devices), ('name', name)] + with self._network_gateway(name=name, devices=devices) as gw: + for k, v in keys: + self.assertEqual(gw[self.gw_resource][k], v) def test_create_network_gateway_no_interface_name(self): - name = 'test-gw' - devices = [{'id': _uuid()}] - exp_devices = devices - exp_devices[0]['interface_name'] = 'breth0' - keys = [('devices', exp_devices), ('name', name)] - with self._network_gateway(name=name, devices=devices) as gw: - for k, v in keys: - self.assertEqual(gw[self.resource][k], v) - - def _test_delete_network_gateway(self, exp_gw_count=0): - name = 'test-gw' - devices = [{'id': _uuid(), 'interface_name': 'xxx'}, - {'id': _uuid(), 'interface_name': 'yyy'}] - with self._network_gateway(name=name, devices=devices): - # Nothing to do here - just let the gateway go - pass - # Verify nothing left on db - session = db_api.get_session() - gw_query = session.query(networkgw_db.NetworkGateway) - dev_query = session.query(networkgw_db.NetworkGatewayDevice) - self.assertEqual(exp_gw_count, gw_query.count()) - self.assertEqual(0, dev_query.count()) + with self._gateway_device() as dev: + name = 'test-gw' + devices = [{'id': dev[self.dev_resource]['id']}] + exp_devices = devices + exp_devices[0]['interface_name'] = 'breth0' + keys = [('devices', exp_devices), ('name', name)] + with self._network_gateway(name=name, devices=devices) as gw: + for k, v in keys: + self.assertEqual(gw[self.gw_resource][k], v) def test_delete_network_gateway(self): - self._test_delete_network_gateway() + with self._gateway_device() as dev: + name = 'test-gw' + device_id = dev[self.dev_resource]['id'] + devices = [{'id': device_id, + 'interface_name': 'xxx'}] + with self._network_gateway(name=name, devices=devices) as gw: + # Nothing to do here - just let the gateway go + gw_id = gw[self.gw_resource]['id'] + # Verify nothing left on db + session = db_api.get_session() + dev_query = session.query( + networkgw_db.NetworkGatewayDevice).filter( + networkgw_db.NetworkGatewayDevice.id == device_id) + self.assertIsNone(dev_query.first()) + gw_query = session.query(networkgw_db.NetworkGateway).filter( + networkgw_db.NetworkGateway.id == gw_id) + self.assertIsNone(gw_query.first()) def test_update_network_gateway(self): with self._network_gateway() as gw: - data = {self.resource: {'name': 'new_name'}} - req = self.new_update_request(networkgw.COLLECTION_NAME, + data = {self.gw_resource: {'name': 'new_name'}} + req = self.new_update_request(networkgw.NETWORK_GATEWAYS, data, - gw[self.resource]['id']) + gw[self.gw_resource]['id']) res = self.deserialize('json', req.get_response(self.ext_api)) - self.assertEqual(res[self.resource]['name'], - data[self.resource]['name']) + self.assertEqual(res[self.gw_resource]['name'], + data[self.gw_resource]['name']) def test_get_network_gateway(self): with self._network_gateway(name='test-gw') as gw: - req = self.new_show_request(networkgw.COLLECTION_NAME, - gw[self.resource]['id']) + req = self.new_show_request(networkgw.NETWORK_GATEWAYS, + gw[self.gw_resource]['id']) res = self.deserialize('json', req.get_response(self.ext_api)) - self.assertEqual(res[self.resource]['name'], - gw[self.resource]['name']) + self.assertEqual(res[self.gw_resource]['name'], + gw[self.gw_resource]['name']) def test_list_network_gateways(self): with self._network_gateway(name='test-gw-1') as gw1: with self._network_gateway(name='test_gw_2') as gw2: - req = self.new_list_request(networkgw.COLLECTION_NAME) + req = self.new_list_request(networkgw.NETWORK_GATEWAYS) res = self.deserialize('json', req.get_response(self.ext_api)) - key = self.resource + 's' + key = self.gw_resource + 's' self.assertEqual(len(res[key]), 2) self.assertEqual(res[key][0]['name'], - gw1[self.resource]['name']) + gw1[self.gw_resource]['name']) self.assertEqual(res[key][1]['name'], - gw2[self.resource]['name']) + gw2[self.gw_resource]['name']) def _test_list_network_gateway_with_multiple_connections( self, expected_gateways=1): with self._network_gateway() as gw: with self.network() as net_1: self._gateway_action('connect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 555) self._gateway_action('connect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 777) - req = self.new_list_request(networkgw.COLLECTION_NAME) + req = self.new_list_request(networkgw.NETWORK_GATEWAYS) res = self.deserialize('json', req.get_response(self.ext_api)) - key = self.resource + 's' + key = self.gw_resource + 's' self.assertEqual(len(res[key]), expected_gateways) for item in res[key]: self.assertIn('ports', item) - if item['id'] == gw[self.resource]['id']: + if item['id'] == gw[self.gw_resource]['id']: gw_ports = item['ports'] self.assertEqual(len(gw_ports), 2) segmentation_ids = [555, 777] @@ -425,11 +619,11 @@ class NetworkGatewayDbTestCase(test_db_plugin.NeutronDbPluginV2TestCase): segmentation_ids.remove(gw_port['segmentation_id']) # Required cleanup self._gateway_action('disconnect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 555) self._gateway_action('disconnect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 777) @@ -449,19 +643,19 @@ class NetworkGatewayDbTestCase(test_db_plugin.NeutronDbPluginV2TestCase): with self._network_gateway() as gw: with self.network() as net_1: self._gateway_action('connect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 555) self._gateway_action('connect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 777) self._gateway_action('disconnect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 555) self._gateway_action('disconnect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 777) @@ -470,19 +664,19 @@ class NetworkGatewayDbTestCase(test_db_plugin.NeutronDbPluginV2TestCase): with self._network_gateway() as gw_2: with self.network() as net_1: self._gateway_action('connect', - gw_1[self.resource]['id'], + gw_1[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 555) self._gateway_action('connect', - gw_2[self.resource]['id'], + gw_2[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 555) self._gateway_action('disconnect', - gw_1[self.resource]['id'], + gw_1[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 555) self._gateway_action('disconnect', - gw_2[self.resource]['id'], + gw_2[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 555) @@ -490,25 +684,25 @@ class NetworkGatewayDbTestCase(test_db_plugin.NeutronDbPluginV2TestCase): with self._network_gateway() as gw: with self.network() as net_1: self._gateway_action('connect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 555) with self.network() as net_2: self._gateway_action('connect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_2['network']['id'], 'vlan', 555, expected_status=exc.HTTPConflict.code) # Clean up - otherwise delete will fail self._gateway_action('disconnect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 555) def test_connect_invalid_network_returns_400(self): with self._network_gateway() as gw: self._gateway_action('connect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], 'hohoho', 'vlan', 555, expected_status=exc.HTTPBadRequest.code) @@ -516,7 +710,7 @@ class NetworkGatewayDbTestCase(test_db_plugin.NeutronDbPluginV2TestCase): def test_connect_unspecified_network_returns_400(self): with self._network_gateway() as gw: self._gateway_action('connect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], None, 'vlan', 555, expected_status=exc.HTTPBadRequest.code) @@ -525,25 +719,25 @@ class NetworkGatewayDbTestCase(test_db_plugin.NeutronDbPluginV2TestCase): with self._network_gateway() as gw: with self.network() as net_1: self._gateway_action('connect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 555) self._gateway_action('connect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 777) # This should raise self._gateway_action('disconnect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'vlan', expected_status=exc.HTTPConflict.code) self._gateway_action('disconnect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 555) self._gateway_action('disconnect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 777) @@ -551,7 +745,7 @@ class NetworkGatewayDbTestCase(test_db_plugin.NeutronDbPluginV2TestCase): with self._network_gateway() as gw: with self.network() as net_1: body = self._gateway_action('connect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 555) # fetch port id and try to delete it @@ -559,7 +753,7 @@ class NetworkGatewayDbTestCase(test_db_plugin.NeutronDbPluginV2TestCase): self._delete('ports', gw_port_id, expected_code=exc.HTTPConflict.code) body = self._gateway_action('disconnect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 555) @@ -567,14 +761,14 @@ class NetworkGatewayDbTestCase(test_db_plugin.NeutronDbPluginV2TestCase): with self._network_gateway() as gw: with self.network() as net_1: self._gateway_action('connect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'flat') - self._delete(networkgw.COLLECTION_NAME, - gw[self.resource]['id'], + self._delete(networkgw.NETWORK_GATEWAYS, + gw[self.gw_resource]['id'], expected_code=exc.HTTPConflict.code) self._gateway_action('disconnect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'flat') @@ -582,25 +776,99 @@ class NetworkGatewayDbTestCase(test_db_plugin.NeutronDbPluginV2TestCase): with self._network_gateway() as gw: with self.network() as net_1: self._gateway_action('connect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 555) self._gateway_action('disconnect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 999, expected_status=exc.HTTPNotFound.code) self._gateway_action('disconnect', - gw[self.resource]['id'], + gw[self.gw_resource]['id'], net_1['network']['id'], 'vlan', 555) + def test_create_gateway_device( + self, expected_status=networkgw_db.STATUS_UNKNOWN): + with self._gateway_device(name='test-dev', + connector_type='stt', + connector_ip='1.1.1.1', + client_certificate='xyz') as dev: + self.assertEqual(dev[self.dev_resource]['name'], 'test-dev') + self.assertEqual(dev[self.dev_resource]['connector_type'], 'stt') + self.assertEqual(dev[self.dev_resource]['connector_ip'], '1.1.1.1') + self.assertEqual(dev[self.dev_resource]['status'], expected_status) + + def test_get_gateway_device( + self, expected_status=networkgw_db.STATUS_UNKNOWN): + with self._gateway_device(name='test-dev', + connector_type='stt', + connector_ip='1.1.1.1', + client_certificate='xyz') as dev: + req = self.new_show_request(networkgw.GATEWAY_DEVICES, + dev[self.dev_resource]['id']) + res = self.deserialize('json', req.get_response(self.ext_api)) + self.assertEqual(res[self.dev_resource]['name'], 'test-dev') + self.assertEqual(res[self.dev_resource]['connector_type'], 'stt') + self.assertEqual(res[self.dev_resource]['connector_ip'], '1.1.1.1') + self.assertEqual(res[self.dev_resource]['status'], expected_status) + + def test_update_gateway_device( + self, expected_status=networkgw_db.STATUS_UNKNOWN): + with self._gateway_device(name='test-dev', + connector_type='stt', + connector_ip='1.1.1.1', + client_certificate='xyz') as dev: + self._update_gateway_device('json', dev[self.dev_resource]['id'], + connector_type='stt', + connector_ip='2.2.2.2', + name='test-dev-upd') + req = self.new_show_request(networkgw.GATEWAY_DEVICES, + dev[self.dev_resource]['id']) + res = self.deserialize('json', req.get_response(self.ext_api)) + + self.assertEqual(res[self.dev_resource]['name'], 'test-dev-upd') + self.assertEqual(res[self.dev_resource]['connector_type'], 'stt') + self.assertEqual(res[self.dev_resource]['connector_ip'], '2.2.2.2') + self.assertEqual(res[self.dev_resource]['status'], expected_status) + + def test_delete_gateway_device(self): + with self._gateway_device(name='test-dev', + connector_type='stt', + connector_ip='1.1.1.1', + client_certificate='xyz') as dev: + # Nothing to do here - just note the device id + dev_id = dev[self.dev_resource]['id'] + # Verify nothing left on db + session = db_api.get_session() + dev_query = session.query(networkgw_db.NetworkGatewayDevice) + dev_query.filter(networkgw_db.NetworkGatewayDevice.id == dev_id) + self.assertIsNone(dev_query.first()) + class TestNetworkGateway(NsxPluginV2TestCase, NetworkGatewayDbTestCase): def setUp(self, plugin=PLUGIN_NAME, ext_mgr=None): cfg.CONF.set_override('api_extensions_path', NSXEXT_PATH) + # Mock l2gwlib calls for gateway devices since this resource is not + # mocked through the fake NVP API client + create_gw_dev_patcher = mock.patch.object( + l2gwlib, 'create_gateway_device') + update_gw_dev_patcher = mock.patch.object( + l2gwlib, 'update_gateway_device') + delete_gw_dev_patcher = mock.patch.object( + l2gwlib, 'delete_gateway_device') + get_gw_dev_status_patcher = mock.patch.object( + l2gwlib, 'get_gateway_device_status') + mock_create_gw_dev = create_gw_dev_patcher.start() + mock_create_gw_dev.return_value = {'uuid': 'callejon'} + update_gw_dev_patcher.start() + delete_gw_dev_patcher.start() + self.mock_get_gw_dev_status = get_gw_dev_status_patcher.start() + + self.addCleanup(mock.patch.stopall) super(TestNetworkGateway, self).setUp(plugin=plugin, ext_mgr=ext_mgr) @@ -608,15 +876,15 @@ class TestNetworkGateway(NsxPluginV2TestCase, name = 'this_is_a_gateway_whose_name_is_longer_than_40_chars' with self._network_gateway(name=name) as nw_gw: # Assert Neutron name is not truncated - self.assertEqual(nw_gw[self.resource]['name'], name) + self.assertEqual(nw_gw[self.gw_resource]['name'], name) def test_update_network_gateway_with_name_calls_backend(self): with mock.patch.object( nsxlib.l2gateway, 'update_l2_gw_service') as mock_update_gw: with self._network_gateway(name='cavani') as nw_gw: - nw_gw_id = nw_gw[self.resource]['id'] - self._update(networkgw.COLLECTION_NAME, nw_gw_id, - {self.resource: {'name': 'higuain'}}) + nw_gw_id = nw_gw[self.gw_resource]['id'] + self._update(networkgw.NETWORK_GATEWAYS, nw_gw_id, + {self.gw_resource: {'name': 'higuain'}}) mock_update_gw.assert_called_once_with( mock.ANY, nw_gw_id, 'higuain') @@ -624,22 +892,22 @@ class TestNetworkGateway(NsxPluginV2TestCase, with mock.patch.object( nsxlib.l2gateway, 'update_l2_gw_service') as mock_update_gw: with self._network_gateway(name='something') as nw_gw: - nw_gw_id = nw_gw[self.resource]['id'] - self._update(networkgw.COLLECTION_NAME, nw_gw_id, - {self.resource: {}}) + nw_gw_id = nw_gw[self.gw_resource]['id'] + self._update(networkgw.NETWORK_GATEWAYS, nw_gw_id, + {self.gw_resource: {}}) self.assertEqual(mock_update_gw.call_count, 0) def test_update_network_gateway_name_exceeds_40_chars(self): new_name = 'this_is_a_gateway_whose_name_is_longer_than_40_chars' with self._network_gateway(name='something') as nw_gw: - nw_gw_id = nw_gw[self.resource]['id'] - self._update(networkgw.COLLECTION_NAME, nw_gw_id, - {self.resource: {'name': new_name}}) - req = self.new_show_request(networkgw.COLLECTION_NAME, + nw_gw_id = nw_gw[self.gw_resource]['id'] + self._update(networkgw.NETWORK_GATEWAYS, nw_gw_id, + {self.gw_resource: {'name': new_name}}) + req = self.new_show_request(networkgw.NETWORK_GATEWAYS, nw_gw_id) res = self.deserialize('json', req.get_response(self.ext_api)) # Assert Neutron name is not truncated - self.assertEqual(new_name, res[self.resource]['name']) + self.assertEqual(new_name, res[self.gw_resource]['name']) # Assert NSX name is truncated self.assertEqual( new_name[:40], @@ -652,49 +920,77 @@ class TestNetworkGateway(NsxPluginV2TestCase, with mock.patch.object(nsxlib.l2gateway, 'create_l2_gw_service', new=raise_nsx_api_exc): - res = self._create_network_gateway( - self.fmt, 'xxx', name='yyy', - devices=[{'id': uuidutils.generate_uuid()}]) + with self._gateway_device() as dev: + res = self._create_network_gateway( + self.fmt, 'xxx', name='yyy', + devices=[{'id': dev[self.dev_resource]['id']}]) self.assertEqual(500, res.status_int) def test_create_network_gateway_nsx_error_returns_409(self): with mock.patch.object(nsxlib.l2gateway, 'create_l2_gw_service', side_effect=api_exc.Conflict): - res = self._create_network_gateway( - self.fmt, 'xxx', name='yyy', - devices=[{'id': uuidutils.generate_uuid()}]) + with self._gateway_device() as dev: + res = self._create_network_gateway( + self.fmt, 'xxx', name='yyy', + devices=[{'id': dev[self.dev_resource]['id']}]) self.assertEqual(409, res.status_int) def test_list_network_gateways(self): with self._network_gateway(name='test-gw-1') as gw1: with self._network_gateway(name='test_gw_2') as gw2: - req = self.new_list_request(networkgw.COLLECTION_NAME) + req = self.new_list_request(networkgw.NETWORK_GATEWAYS) res = self.deserialize('json', req.get_response(self.ext_api)) # We expect the default gateway too - key = self.resource + 's' + key = self.gw_resource + 's' self.assertEqual(len(res[key]), 3) self.assertEqual(res[key][0]['default'], True) self.assertEqual(res[key][1]['name'], - gw1[self.resource]['name']) + gw1[self.gw_resource]['name']) self.assertEqual(res[key][2]['name'], - gw2[self.resource]['name']) + gw2[self.gw_resource]['name']) def test_list_network_gateway_with_multiple_connections(self): self._test_list_network_gateway_with_multiple_connections( expected_gateways=2) - def test_delete_network_gateway(self): - # The default gateway must still be there - self._test_delete_network_gateway(1) - def test_show_network_gateway_nsx_error_returns_404(self): invalid_id = 'b5afd4a9-eb71-4af7-a082-8fc625a35b61' - req = self.new_show_request(networkgw.COLLECTION_NAME, invalid_id) + req = self.new_show_request(networkgw.NETWORK_GATEWAYS, invalid_id) res = req.get_response(self.ext_api) self.assertEqual(exc.HTTPNotFound.code, res.status_int) + def test_create_gateway_device(self): + self.mock_get_gw_dev_status.return_value = True + super(TestNetworkGateway, self).test_create_gateway_device( + expected_status=networkgw_db.STATUS_ACTIVE) + + def test_create_gateway_device_status_down(self): + self.mock_get_gw_dev_status.return_value = False + super(TestNetworkGateway, self).test_create_gateway_device( + expected_status=networkgw_db.STATUS_DOWN) + + def test_get_gateway_device(self): + self.mock_get_gw_dev_status.return_value = True + super(TestNetworkGateway, self).test_get_gateway_device( + expected_status=networkgw_db.STATUS_ACTIVE) + + def test_get_gateway_device_status_down(self): + self.mock_get_gw_dev_status.return_value = False + super(TestNetworkGateway, self).test_get_gateway_device( + expected_status=networkgw_db.STATUS_DOWN) + + def test_update_gateway_device(self): + self.mock_get_gw_dev_status.return_value = True + super(TestNetworkGateway, self).test_update_gateway_device( + expected_status=networkgw_db.STATUS_ACTIVE) + + def test_update_gateway_device_status_down(self): + self.mock_get_gw_dev_status.return_value = False + super(TestNetworkGateway, self).test_update_gateway_device( + expected_status=networkgw_db.STATUS_DOWN) + class TestNetworkGatewayPlugin(db_base_plugin_v2.NeutronDbPluginV2, networkgw_db.NetworkGatewayMixin): diff --git a/neutron/tests/unit/vmware/nsxlib/test_l2gateway.py b/neutron/tests/unit/vmware/nsxlib/test_l2gateway.py index d122ad0510..36b8d26a89 100644 --- a/neutron/tests/unit/vmware/nsxlib/test_l2gateway.py +++ b/neutron/tests/unit/vmware/nsxlib/test_l2gateway.py @@ -14,7 +14,11 @@ # limitations under the License. # +import mock + +from neutron.openstack.common import jsonutils from neutron.plugins.vmware.api_client import exception +from neutron.plugins.vmware.common import utils as nsx_utils from neutron.plugins.vmware import nsxlib from neutron.plugins.vmware.nsxlib import l2gateway as l2gwlib from neutron.plugins.vmware.nsxlib import switch as switchlib @@ -145,3 +149,148 @@ class L2GatewayTestCase(base.NsxlibTestCase): self.assertIn('LogicalPortAttachment', resp_obj) self.assertEqual(resp_obj['LogicalPortAttachment']['type'], 'L2GatewayAttachment') + + def _create_expected_req_body(self, display_name, neutron_id, + connector_type, connector_ip, + client_certificate): + body = { + "display_name": display_name, + "tags": [{"tag": neutron_id, "scope": "q_gw_dev_id"}, + {"tag": 'fake_tenant', "scope": "os_tid"}, + {"tag": nsx_utils.NEUTRON_VERSION, + "scope": "quantum"}], + "transport_connectors": [ + {"transport_zone_uuid": 'fake_tz_uuid', + "ip_address": connector_ip, + "type": '%sConnector' % connector_type}], + "admin_status_enabled": True + } + if client_certificate: + body["credential"] = { + "client_certificate": { + "pem_encoded": client_certificate}, + "type": "SecurityCertificateCredential"} + return body + + def test_create_gw_device(self): + # NOTE(salv-orlando): This unit test mocks backend calls rather than + # leveraging the fake NVP API client + display_name = 'fake-device' + neutron_id = 'whatever' + connector_type = 'stt' + connector_ip = '1.1.1.1' + client_certificate = 'this_should_be_a_certificate' + with mock.patch.object(l2gwlib, 'do_request') as request_mock: + expected_req_body = self._create_expected_req_body( + display_name, neutron_id, connector_type.upper(), + connector_ip, client_certificate) + l2gwlib.create_gateway_device( + self.fake_cluster, 'fake_tenant', display_name, neutron_id, + 'fake_tz_uuid', connector_type, connector_ip, + client_certificate) + request_mock.assert_called_once_with( + "POST", + "/ws.v1/transport-node", + jsonutils.dumps(expected_req_body), + cluster=self.fake_cluster) + + def test_update_gw_device(self): + # NOTE(salv-orlando): This unit test mocks backend calls rather than + # leveraging the fake NVP API client + display_name = 'fake-device' + neutron_id = 'whatever' + connector_type = 'stt' + connector_ip = '1.1.1.1' + client_certificate = 'this_should_be_a_certificate' + with mock.patch.object(l2gwlib, 'do_request') as request_mock: + expected_req_body = self._create_expected_req_body( + display_name, neutron_id, connector_type.upper(), + connector_ip, client_certificate) + l2gwlib.update_gateway_device( + self.fake_cluster, 'whatever', 'fake_tenant', + display_name, neutron_id, + 'fake_tz_uuid', connector_type, connector_ip, + client_certificate) + + request_mock.assert_called_once_with( + "PUT", + "/ws.v1/transport-node/whatever", + jsonutils.dumps(expected_req_body), + cluster=self.fake_cluster) + + def test_update_gw_device_without_certificate(self): + # NOTE(salv-orlando): This unit test mocks backend calls rather than + # leveraging the fake NVP API client + display_name = 'fake-device' + neutron_id = 'whatever' + connector_type = 'stt' + connector_ip = '1.1.1.1' + with mock.patch.object(l2gwlib, 'do_request') as request_mock: + expected_req_body = self._create_expected_req_body( + display_name, neutron_id, connector_type.upper(), + connector_ip, None) + l2gwlib.update_gateway_device( + self.fake_cluster, 'whatever', 'fake_tenant', + display_name, neutron_id, + 'fake_tz_uuid', connector_type, connector_ip, + client_certificate=None) + + request_mock.assert_called_once_with( + "PUT", + "/ws.v1/transport-node/whatever", + jsonutils.dumps(expected_req_body), + cluster=self.fake_cluster) + + def test_get_gw_device_status(self): + # NOTE(salv-orlando): This unit test mocks backend calls rather than + # leveraging the fake NVP API client + with mock.patch.object(l2gwlib, 'do_request') as request_mock: + l2gwlib.get_gateway_device_status(self.fake_cluster, 'whatever') + request_mock.assert_called_once_with( + "GET", + "/ws.v1/transport-node/whatever/status", + cluster=self.fake_cluster) + + def test_get_gw_devices_status(self): + # NOTE(salv-orlando): This unit test mocks backend calls rather than + # leveraging the fake NVP API client + with mock.patch.object(nsxlib, 'do_request') as request_mock: + request_mock.return_value = { + 'results': [], + 'page_cursor': None, + 'result_count': 0} + l2gwlib.get_gateway_devices_status(self.fake_cluster) + request_mock.assert_called_once_with( + "GET", + ("/ws.v1/transport-node?fields=uuid,tags&" + "relations=TransportNodeStatus&" + "_page_length=1000&tag_scope=quantum"), + cluster=self.fake_cluster) + + def test_get_gw_devices_status_filter_by_tenant(self): + # NOTE(salv-orlando): This unit test mocks backend calls rather than + # leveraging the fake NVP API client + with mock.patch.object(nsxlib, 'do_request') as request_mock: + request_mock.return_value = { + 'results': [], + 'page_cursor': None, + 'result_count': 0} + l2gwlib.get_gateway_devices_status(self.fake_cluster, + tenant_id='ssc_napoli') + request_mock.assert_called_once_with( + "GET", + ("/ws.v1/transport-node?fields=uuid,tags&" + "relations=TransportNodeStatus&" + "tag_scope=os_tid&tag=ssc_napoli&" + "_page_length=1000&tag_scope=quantum"), + cluster=self.fake_cluster) + + def test_delete_gw_device(self): + # NOTE(salv-orlando): This unit test mocks backend calls rather than + # leveraging the fake NVP API client + with mock.patch.object(l2gwlib, 'do_request') as request_mock: + l2gwlib.delete_gateway_device(self.fake_cluster, 'whatever') + request_mock.assert_called_once_with( + "DELETE", + "/ws.v1/transport-node/whatever", + cluster=self.fake_cluster)