]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Add support for tenant-provided NSX gateways devices
authorSalvatore Orlando <salv.orlando@gmail.com>
Tue, 18 Feb 2014 00:20:53 +0000 (16:20 -0800)
committerSalvatore Orlando <salv.orlando@gmail.com>
Fri, 7 Mar 2014 22:00:03 +0000 (23:00 +0100)
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
neutron/db/migration/alembic_migrations/versions/19180cf98af6_nsx_gw_devices.py [new file with mode: 0644]
neutron/plugins/vmware/common/nsx_utils.py
neutron/plugins/vmware/common/utils.py
neutron/plugins/vmware/dbexts/networkgw_db.py
neutron/plugins/vmware/extensions/networkgw.py
neutron/plugins/vmware/nsxlib/l2gateway.py
neutron/plugins/vmware/plugins/base.py
neutron/tests/unit/vmware/extensions/test_networkgw.py
neutron/tests/unit/vmware/nsxlib/test_l2gateway.py

index d908c5ba0fc74571ad15c64c61aa3eb78fbe1f61..ab90bd683b581eb366b6400759f41b5c58292689 100644 (file)
@@ -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 (file)
index 0000000..1e5ecc5
--- /dev/null
@@ -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')
index 55d8b747522dbaaa1e24093f0e40db9888231b35..c2c2b7f28c147d320cbe294b4fdf3996d44c6995 100644 (file)
 #    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"))
index 48773f63f06b300e50e189497d7621178b6dcbd5..67d719d7dd1b0312de9a2758140c8c4d7bc6fd69 100644 (file)
@@ -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()])
index 58d14c55661af973e7e0f3e3cfad151db9bfdb9e..5609a0867f49818faa148f7fb441881f399247be 100644 (file)
@@ -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)
index 57783352258a307080e84bf91f3cfaf25f49c26b..3d8ea8807ab582fea35a2362726b96af28fc8c52 100644 (file)
@@ -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'}
-
-        # 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)]
+
+        member_actions = {
+            GATEWAY_RESOURCE_NAME.replace('_', '-'): {
+                'connect_network': 'PUT',
+                'disconnect_network': 'PUT'}}
+
+        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
index 46128acbead28098e44cc9ff01e4f487c5525876..80397d51d76f82c156464abe5a4a197cc442fe90 100644 (file)
@@ -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
index 2b832d1324e20e395ab27c89d59f535cdfe70de0..886938ac2e8a3af2cc55d241fe366e518bdaba7c 100644 (file)
@@ -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.
 
index 25c542f57adb6c9239f4daca0272bbdb54b1cc03..a0b26101b5ce9054ddc20dc0aa34267e92e68b92 100644 (file)
@@ -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):
index d122ad05104b9bae1367a8f9827fd31e00c319aa..36b8d26a8998a026a9902b3c0b9acc55ac7c15e9 100644 (file)
 # 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)