Merge "add unittest for delete resource types association"
diff --git a/releasenotes/notes/add-subnet-id-config-option-fac3d6f12abfc171.yaml b/releasenotes/notes/add-subnet-id-config-option-fac3d6f12abfc171.yaml
new file mode 100644
index 0000000..a1bd4c5
--- /dev/null
+++ b/releasenotes/notes/add-subnet-id-config-option-fac3d6f12abfc171.yaml
@@ -0,0 +1,8 @@
+---
+features:
+  - A new config option 'subnet_id' is added to section
+    'network' to specify subnet which should be used for
+    allocation of IPs for VMs created during testing.
+    It should be used when the tested network contains more
+    than one subnet otherwise test of external connectivity
+    will fail. (Fixes bug #1856671)
diff --git a/releasenotes/notes/introduce-attachments-client-add-show-attachment-api-c3111f7e560a87b3.yaml b/releasenotes/notes/introduce-attachments-client-add-show-attachment-api-c3111f7e560a87b3.yaml
new file mode 100644
index 0000000..a058137
--- /dev/null
+++ b/releasenotes/notes/introduce-attachments-client-add-show-attachment-api-c3111f7e560a87b3.yaml
@@ -0,0 +1,8 @@
+---
+features:
+  - |
+    A new attachments client library has been introduced for the volume
+    service.
+
+    Initially only the show_attachment API is provided. This API requires a
+    minimum volume API microversion of ``3.27``.
diff --git a/roles/run-tempest/README.rst b/roles/run-tempest/README.rst
index 1f7fb70..91b0b5f 100644
--- a/roles/run-tempest/README.rst
+++ b/roles/run-tempest/README.rst
@@ -20,14 +20,12 @@
    It works only when used with some specific tox environments
    ('all', 'all-plugin'.)
 
-   Multi-line and commented regexs can be achieved by doing this:
+   In the following example only api scenario and third party tests
+   will be executed.
 
        ::
            vars:
-             tempest_test_regex: |
-               (?x)    # Ignore comments and whitespaces
-               # Line with only a comment.
-               (tempest\.(api|scenario|thirdparty)).*$    # Run only api scenario and third party
+             tempest_test_regex: (tempest\.(api|scenario|thirdparty)).*$
 
 .. zuul:rolevar:: tempest_test_blacklist
 
@@ -48,14 +46,9 @@
    It works only when used with some specific tox environments
    ('all', 'all-plugin'.)
 
-   Multi-line and commented regexs can be achieved by doing this:
-
        ::
            vars:
-             tempest_black_regex: |
-               (?x)    # Ignore comments and whitespaces
-               # Line with only a comment.
-               (tempest.api.identity).*$
+             tempest_black_regex: (tempest.api.identity).*$
 
 .. zuul:rolevar:: tox_extra_args
    :default: ''
diff --git a/tempest/api/volume/admin/test_volume_quota_classes.py b/tempest/api/volume/admin/test_volume_quota_classes.py
index 75dca41..ee52354 100644
--- a/tempest/api/volume/admin/test_volume_quota_classes.py
+++ b/tempest/api/volume/admin/test_volume_quota_classes.py
@@ -44,12 +44,10 @@
 
     @decorators.idempotent_id('abb9198e-67d0-4b09-859f-4f4a1418f176')
     def test_show_default_quota(self):
+        # response body is validated by schema
         default_quotas = self.admin_quota_classes_client.show_quota_class_set(
             'default')['quota_class_set']
-        self.assertIn('id', default_quotas)
         self.assertEqual('default', default_quotas.pop('id'))
-        for key in QUOTA_KEYS:
-            self.assertIn(key, default_quotas)
 
     @decorators.idempotent_id('a7644c63-2669-467a-b00e-452dd5c5397b')
     def test_update_default_quota(self):
diff --git a/tempest/api/volume/admin/test_volume_types.py b/tempest/api/volume/admin/test_volume_types.py
index 9e24176..c1ceeb7 100644
--- a/tempest/api/volume/admin/test_volume_types.py
+++ b/tempest/api/volume/admin/test_volume_types.py
@@ -92,15 +92,12 @@
                   'extra_specs': extra_specs,
                   'os-volume-type-access:is_public': True}
         body = self.create_volume_type(**params)
-        self.assertIn('name', body)
         self.assertEqual(name, body['name'],
                          "The created volume_type name is not equal "
                          "to the requested name")
         self.assertEqual(description, body['description'],
                          "The created volume_type_description name is "
                          "not equal to the requested name")
-        self.assertIsNotNone(body['id'],
-                             "Field volume_type id is empty or not found.")
         fetched_volume_type = self.admin_volume_types_client.show_volume_type(
             body['id'])['volume_type']
         self.assertEqual(name, fetched_volume_type['name'],
diff --git a/tempest/clients.py b/tempest/clients.py
index 6aed92e..1db93a0 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -263,6 +263,8 @@
                 self.volume_v3.MessagesClient())
             self.volume_versions_client_latest = (
                 self.volume_v3.VersionsClient())
+            self.attachments_client_latest = (
+                self.volume_v3.AttachmentsClient())
 
             # TODO(gmann): Below alias for service clients have been
             # deprecated and will be removed in future. Start using the alias
diff --git a/tempest/cmd/cleanup_service.py b/tempest/cmd/cleanup_service.py
index 2b35ebf..9b591bc 100644
--- a/tempest/cmd/cleanup_service.py
+++ b/tempest/cmd/cleanup_service.py
@@ -13,6 +13,7 @@
 #    under the License.
 
 from oslo_log import log as logging
+from six.moves.urllib import parse as urllib
 
 from tempest import clients
 from tempest.common import credentials_factory as credentials
@@ -362,6 +363,25 @@
         self.data['compute_quotas'] = quotas['absolute']
 
 
+class NetworkQuotaService(BaseService):
+    def __init__(self, manager, **kwargs):
+        super(NetworkQuotaService, self).__init__(kwargs)
+        self.client = manager.network_quotas_client
+
+    def delete(self):
+        client = self.client
+        try:
+            client.reset_quotas(self.project_id)
+        except Exception:
+            LOG.exception("Delete Network Quotas exception for 'project %s'.",
+                          self.project_id)
+
+    def dry_run(self):
+        resp = [quota for quota in self.client.list_quotas()['quotas']
+                if quota['project_id'] == self.project_id]
+        self.data['network_quotas'] = resp
+
+
 # Begin network service classes
 class BaseNetworkService(BaseService):
     def __init__(self, manager, **kwargs):
@@ -814,7 +834,15 @@
 
     def list(self):
         client = self.client
-        images = client.list_images(params={"all_tenants": True})['images']
+        response = client.list_images()
+        images = []
+        images.extend(response['images'])
+        while 'next' in response:
+            parsed = urllib.urlparse(response['next'])
+            marker = urllib.parse_qs(parsed.query)['marker'][0]
+            response = client.list_images(params={"marker": marker})
+            images.extend(response['images'])
+
         if not self.is_save_state:
             images = [image for image in images if image['id']
                       not in self.saved_state_json['images'].keys()]
@@ -1048,6 +1076,7 @@
         resource_cleanup_services.append(NetworkService)
         resource_cleanup_services.append(NetworkSecGroupService)
         resource_cleanup_services.append(NetworkSubnetPoolsService)
+        resource_cleanup_services.append(NetworkQuotaService)
     if IS_CINDER:
         resource_cleanup_services.append(SnapshotService)
         resource_cleanup_services.append(VolumeService)
diff --git a/tempest/config.py b/tempest/config.py
index 5a2d722..9685745 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -699,6 +699,11 @@
     cfg.StrOpt('floating_network_name',
                help="Default floating network name. Used to allocate floating "
                     "IPs when neutron is enabled."),
+    cfg.StrOpt('subnet_id',
+               default="",
+               help="Subnet id of subnet which is used for allocation of "
+                    "floating IPs. Specify when two or more subnets are "
+                    "present in network."),
     cfg.StrOpt('public_router_id',
                default="",
                help="Id of the public router that provides external "
diff --git a/tempest/lib/api_schema/response/volume/quota_classes.py b/tempest/lib/api_schema/response/volume/quota_classes.py
new file mode 100644
index 0000000..1a575d2
--- /dev/null
+++ b/tempest/lib/api_schema/response/volume/quota_classes.py
@@ -0,0 +1,68 @@
+# Copyright 2018 ZTE Corporation.  All rights reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+show_quota_classes = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'quota_class_set': {
+                'type': 'object',
+                'properties': {
+                    'id': {'type': 'string', 'format': 'uuid'},
+                    'volumes': {'type': 'integer'},
+                    'snapshots': {'type': 'integer'},
+                    'backups': {'type': 'integer'},
+                    'groups': {'type': 'integer'},
+                    'per_volume_gigabytes': {'type': 'integer'},
+                    'gigabytes': {'type': 'integer'},
+                    'backup_gigabytes': {'type': 'integer'},
+                },
+                # for volumes_{volume_type}, etc
+                "additionalProperties": {'type': 'integer'},
+                'required': ['id', 'volumes', 'snapshots', 'backups',
+                             'per_volume_gigabytes', 'gigabytes',
+                             'backup_gigabytes'],
+            }
+        },
+        'required': ['quota_class_set']
+    }
+}
+
+update_quota_classes = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'quota_class_set': {
+                'type': 'object',
+                'properties': {
+                    'volumes': {'type': 'integer'},
+                    'snapshots': {'type': 'integer'},
+                    'backups': {'type': 'integer'},
+                    'groups': {'type': 'integer'},
+                    'per_volume_gigabytes': {'type': 'integer'},
+                    'gigabytes': {'type': 'integer'},
+                    'backup_gigabytes': {'type': 'integer'},
+                },
+                # for volumes_{volume_type}, etc
+                "additionalProperties": {'type': 'integer'},
+                'required': ['volumes', 'snapshots', 'backups',
+                             'per_volume_gigabytes', 'gigabytes',
+                             'backup_gigabytes'],
+            }
+        },
+        'required': ['quota_class_set']
+    }
+}
diff --git a/tempest/lib/api_schema/response/volume/volume_types.py b/tempest/lib/api_schema/response/volume/volume_types.py
new file mode 100644
index 0000000..51b3a72
--- /dev/null
+++ b/tempest/lib/api_schema/response/volume/volume_types.py
@@ -0,0 +1,176 @@
+# Copyright 2018 ZTE Corporation.  All rights reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+extra_specs_info = {
+    'type': 'object',
+    'patternProperties': {
+        '^.+$': {'type': 'string'}
+    }
+}
+
+common_show_volume_type = {
+    'type': 'object',
+    'properties': {
+        'extra_specs': extra_specs_info,
+        'name': {'type': 'string'},
+        'is_public': {'type': 'boolean'},
+        'description': {'type': ['string', 'null']},
+        'id': {'type': 'string', 'format': 'uuid'},
+        'os-volume-type-access:is_public': {'type': 'boolean'},
+        'qos_specs_id': {'type': ['string', 'null'], 'format': 'uuid'}
+    },
+    'additionalProperties': False,
+    'required': ['name', 'is_public', 'description', 'id',
+                 'os-volume-type-access:is_public']
+}
+
+show_volume_type = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'volume_type': common_show_volume_type,
+        },
+        'additionalProperties': False,
+        'required': ['volume_type']
+    }
+}
+
+delete_volume_type = {'status_code': [202]}
+
+create_volume_type = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'volume_type': {
+                'type': 'object',
+                'properties': {
+                    'extra_specs': extra_specs_info,
+                    'name': {'type': 'string'},
+                    'is_public': {'type': 'boolean'},
+                    'description': {'type': ['string', 'null']},
+                    'id': {'type': 'string', 'format': 'uuid'},
+                    'os-volume-type-access:is_public': {'type': 'boolean'}
+                },
+                'additionalProperties': False,
+                'required': ['name', 'is_public', 'id',
+                             'description', 'os-volume-type-access:is_public']
+            },
+        },
+        'additionalProperties': False,
+        'required': ['volume_type']
+    }
+}
+
+list_volume_types = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'volume_types': {
+                'type': 'array',
+                'items': common_show_volume_type
+            }
+        },
+        'additionalProperties': False,
+        'required': ['volume_types']
+    }
+}
+
+list_volume_types_extra_specs = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'extra_specs': extra_specs_info
+        },
+        'additionalProperties': False,
+        'required': ['extra_specs']
+    }
+}
+
+show_volume_types_extra_specs = {
+    'status_code': [200],
+    'response_body': extra_specs_info
+}
+
+create_volume_types_extra_specs = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'extra_specs': extra_specs_info
+        },
+        'additionalProperties': False,
+        'required': ['extra_specs']
+    }
+}
+
+delete_volume_types_extra_specs = {'status_code': [202]}
+
+update_volume_types = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'volume_type': {
+                'type': 'object',
+                'properties': {
+                    'extra_specs': extra_specs_info,
+                    'name': {'type': 'string'},
+                    'is_public': {'type': 'boolean'},
+                    'description': {'type': ['string', 'null']},
+                    'id': {'type': 'string', 'format': 'uuid'}
+                },
+                'additionalProperties': False,
+                'required': ['name', 'is_public', 'description', 'id']
+            },
+        },
+        'additionalProperties': False,
+        'required': ['volume_type']
+    }
+}
+
+update_volume_type_extra_specs = {
+    'status_code': [200],
+    'response_body': extra_specs_info
+}
+
+add_type_access = {'status_code': [202]}
+
+remove_type_access = {'status_code': [202]}
+
+list_type_access = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'volume_type_access': {
+                'type': 'array',
+                'items': {
+                    'type': 'object',
+                    'properties': {
+                        'volume_type_id': {'type': 'string', 'format': 'uuid'},
+                        'project_id': {'type': 'string', 'format': 'uuid'},
+                    },
+                    'additionalProperties': False,
+                    'required': ['volume_type_id', 'project_id']
+                }
+            }
+        },
+        'additionalProperties': False,
+        'required': ['volume_type_access']
+    }
+}
diff --git a/tempest/lib/common/ssh.py b/tempest/lib/common/ssh.py
index 2ac1605..3a05f27 100644
--- a/tempest/lib/common/ssh.py
+++ b/tempest/lib/common/ssh.py
@@ -75,6 +75,11 @@
         self.channel_timeout = float(channel_timeout)
         self.buf_size = 1024
         self.proxy_client = proxy_client
+        if (self.proxy_client and self.proxy_client.host == self.host and
+                self.proxy_client.port == self.port and
+                self.proxy_client.username == self.username):
+            raise exceptions.SSHClientProxyClientLoop(
+                host=self.host, port=self.port, username=self.username)
         self._proxy_conn = None
 
     def _get_ssh_connection(self, sleep=1.5, backoff=1):
@@ -114,8 +119,10 @@
                 ssh.close()
                 if self._is_timed_out(_start_time):
                     LOG.exception("Failed to establish authenticated ssh"
-                                  " connection to %s@%s after %d attempts",
-                                  self.username, self.host, attempts)
+                                  " connection to %s@%s after %d attempts. "
+                                  "Proxy client: %s",
+                                  self.username, self.host, attempts,
+                                  self._get_proxy_client_info())
                     raise exceptions.SSHTimeout(host=self.host,
                                                 user=self.username,
                                                 password=self.password)
@@ -219,3 +226,13 @@
         cmd = 'nc %s %s' % (self.host, self.port)
         chan.exec_command(cmd)
         return chan
+
+    def _get_proxy_client_info(self):
+        if not self.proxy_client:
+            return 'no proxy client'
+        nested_pclient = self.proxy_client._get_proxy_client_info()
+        return ('%(username)s@%(host)s:%(port)s, nested proxy client: '
+                '%(nested_pclient)s' % {'username': self.proxy_client.username,
+                                        'host': self.proxy_client.host,
+                                        'port': self.proxy_client.port,
+                                        'nested_pclient': nested_pclient})
diff --git a/tempest/lib/exceptions.py b/tempest/lib/exceptions.py
index b25b4b2..84b7ee6 100644
--- a/tempest/lib/exceptions.py
+++ b/tempest/lib/exceptions.py
@@ -251,6 +251,11 @@
                "stdout:\n%(stdout)s")
 
 
+class SSHClientProxyClientLoop(TempestException):
+    message = ("SSH client proxy client has same host: %(host)s, port: "
+               "%(port)s and username: %(username)s as parent")
+
+
 class UnknownServiceClient(TempestException):
     message = "Service clients named %(services)s are not known"
 
diff --git a/tempest/lib/services/image/v2/namespace_tags_client.py b/tempest/lib/services/image/v2/namespace_tags_client.py
index cf63e50..4315f16 100644
--- a/tempest/lib/services/image/v2/namespace_tags_client.py
+++ b/tempest/lib/services/image/v2/namespace_tags_client.py
@@ -116,10 +116,6 @@
         url = 'metadefs/namespaces/%s/tags' % namespace
         resp, _ = self.delete(url)
 
-        # NOTE(rosmaita): Bug 1656183 fixed the success response code for
-        # this call to make it consistent with the other metadefs delete
-        # calls.  Accept both codes in case tempest is being run against
-        # an old Glance.
-        self.expected_success([200, 204], resp.status)
+        self.expected_success(204, resp.status)
 
         return rest_client.ResponseBody(resp)
diff --git a/tempest/lib/services/volume/v3/__init__.py b/tempest/lib/services/volume/v3/__init__.py
index a1b7de3..e2fa836 100644
--- a/tempest/lib/services/volume/v3/__init__.py
+++ b/tempest/lib/services/volume/v3/__init__.py
@@ -11,6 +11,7 @@
 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 # License for the specific language governing permissions and limitations under
 # the License.
+from tempest.lib.services.volume.v3.attachments_client import AttachmentsClient
 from tempest.lib.services.volume.v3.availability_zone_client \
     import AvailabilityZoneClient
 from tempest.lib.services.volume.v3.backups_client import BackupsClient
@@ -43,12 +44,11 @@
 from tempest.lib.services.volume.v3.volume_manage_client import \
     VolumeManageClient
 from tempest.lib.services.volume.v3.volumes_client import VolumesClient
-
-__all__ = ['AvailabilityZoneClient', 'BackupsClient', 'BaseClient',
-           'CapabilitiesClient', 'EncryptionTypesClient', 'ExtensionsClient',
-           'GroupSnapshotsClient', 'GroupTypesClient', 'GroupsClient',
-           'HostsClient', 'LimitsClient', 'MessagesClient', 'QosSpecsClient',
-           'QuotaClassesClient', 'QuotasClient', 'SchedulerStatsClient',
-           'ServicesClient', 'SnapshotManageClient', 'SnapshotsClient',
-           'TransfersClient', 'TypesClient', 'VersionsClient',
-           'VolumeManageClient', 'VolumesClient']
+__all__ = ['AttachmentsClient', 'AvailabilityZoneClient', 'BackupsClient',
+           'BaseClient', 'CapabilitiesClient', 'EncryptionTypesClient',
+           'ExtensionsClient', 'GroupSnapshotsClient', 'GroupTypesClient',
+           'GroupsClient', 'HostsClient', 'LimitsClient', 'MessagesClient',
+           'QosSpecsClient', 'QuotaClassesClient', 'QuotasClient',
+           'SchedulerStatsClient', 'ServicesClient', 'SnapshotManageClient',
+           'SnapshotsClient', 'TransfersClient', 'TypesClient',
+           'VersionsClient', 'VolumeManageClient', 'VolumesClient']
diff --git a/tempest/lib/services/volume/v3/attachments_client.py b/tempest/lib/services/volume/v3/attachments_client.py
new file mode 100644
index 0000000..5e448f7
--- /dev/null
+++ b/tempest/lib/services/volume/v3/attachments_client.py
@@ -0,0 +1,28 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+from tempest.lib.services.volume import base_client
+
+
+class AttachmentsClient(base_client.BaseClient):
+    """Client class to send CRUD attachment V3 API requests"""
+
+    def show_attachment(self, attachment_id):
+        """Show volume attachment."""
+        url = "attachments/%s" % (attachment_id)
+        resp, body = self.get(url)
+        body = json.loads(body)
+        self.expected_success(200, resp.status)
+        return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/quota_classes_client.py b/tempest/lib/services/volume/v3/quota_classes_client.py
index cf03918..ff62f0c 100644
--- a/tempest/lib/services/volume/v3/quota_classes_client.py
+++ b/tempest/lib/services/volume/v3/quota_classes_client.py
@@ -15,6 +15,7 @@
 
 from oslo_serialization import jsonutils as json
 
+from tempest.lib.api_schema.response.volume import quota_classes as schema
 from tempest.lib.common import rest_client
 
 
@@ -30,8 +31,8 @@
         """
         url = 'os-quota-class-sets/%s' % quota_class_id
         resp, body = self.get(url)
-        self.expected_success(200, resp.status)
         body = json.loads(body)
+        self.validate_response(schema.show_quota_classes, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def update_quota_class_set(self, quota_class_id, **kwargs):
@@ -44,6 +45,6 @@
         url = 'os-quota-class-sets/%s' % quota_class_id
         put_body = json.dumps({'quota_class_set': kwargs})
         resp, body = self.put(url, put_body)
-        self.expected_success(200, resp.status)
         body = json.loads(body)
+        self.validate_response(schema.update_quota_classes, resp, body)
         return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/types_client.py b/tempest/lib/services/volume/v3/types_client.py
index 705d319..7fa24a4 100644
--- a/tempest/lib/services/volume/v3/types_client.py
+++ b/tempest/lib/services/volume/v3/types_client.py
@@ -16,6 +16,7 @@
 from oslo_serialization import jsonutils as json
 from six.moves.urllib import parse as urllib
 
+from tempest.lib.api_schema.response.volume import volume_types as schema
 from tempest.lib.common import rest_client
 from tempest.lib import exceptions as lib_exc
 
@@ -48,7 +49,7 @@
 
         resp, body = self.get(url)
         body = json.loads(body)
-        self.expected_success(200, resp.status)
+        self.validate_response(schema.list_volume_types, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def show_volume_type(self, volume_type_id):
@@ -61,7 +62,7 @@
         url = "types/%s" % volume_type_id
         resp, body = self.get(url)
         body = json.loads(body)
-        self.expected_success(200, resp.status)
+        self.validate_response(schema.show_volume_type, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def create_volume_type(self, **kwargs):
@@ -74,7 +75,7 @@
         post_body = json.dumps({'volume_type': kwargs})
         resp, body = self.post('types', post_body)
         body = json.loads(body)
-        self.expected_success(200, resp.status)
+        self.validate_response(schema.create_volume_type, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def delete_volume_type(self, volume_type_id):
@@ -85,7 +86,7 @@
         https://docs.openstack.org/api-ref/block-storage/v3/index.html#delete-a-volume-type
         """
         resp, body = self.delete("types/%s" % volume_type_id)
-        self.expected_success(202, resp.status)
+        self.validate_response(schema.delete_volume_type, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def list_volume_types_extra_specs(self, volume_type_id, **params):
@@ -101,7 +102,8 @@
 
         resp, body = self.get(url)
         body = json.loads(body)
-        self.expected_success(200, resp.status)
+        self.validate_response(
+            schema.list_volume_types_extra_specs, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def show_volume_type_extra_specs(self, volume_type_id, extra_specs_name):
@@ -109,7 +111,8 @@
         url = "types/%s/extra_specs/%s" % (volume_type_id, extra_specs_name)
         resp, body = self.get(url)
         body = json.loads(body)
-        self.expected_success(200, resp.status)
+        self.validate_response(
+            schema.show_volume_types_extra_specs, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def create_volume_type_extra_specs(self, volume_type_id, extra_specs):
@@ -122,14 +125,16 @@
         post_body = json.dumps({'extra_specs': extra_specs})
         resp, body = self.post(url, post_body)
         body = json.loads(body)
-        self.expected_success(200, resp.status)
+        self.validate_response(
+            schema.create_volume_types_extra_specs, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def delete_volume_type_extra_specs(self, volume_type_id, extra_spec_name):
         """Deletes the specified volume type extra spec."""
         resp, body = self.delete("types/%s/extra_specs/%s" % (
             volume_type_id, extra_spec_name))
-        self.expected_success(202, resp.status)
+        self.validate_response(
+            schema.delete_volume_types_extra_specs, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def update_volume_type(self, volume_type_id, **kwargs):
@@ -142,7 +147,7 @@
         put_body = json.dumps({'volume_type': kwargs})
         resp, body = self.put('types/%s' % volume_type_id, put_body)
         body = json.loads(body)
-        self.expected_success(200, resp.status)
+        self.validate_response(schema.update_volume_types, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def update_volume_type_extra_specs(self, volume_type_id, extra_spec_name,
@@ -162,7 +167,8 @@
         put_body = json.dumps(extra_specs)
         resp, body = self.put(url, put_body)
         body = json.loads(body)
-        self.expected_success(200, resp.status)
+        self.validate_response(
+            schema.update_volume_type_extra_specs, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def add_type_access(self, volume_type_id, **kwargs):
@@ -175,7 +181,7 @@
         post_body = json.dumps({'addProjectAccess': kwargs})
         url = 'types/%s/action' % volume_type_id
         resp, body = self.post(url, post_body)
-        self.expected_success(202, resp.status)
+        self.validate_response(schema.add_type_access, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def remove_type_access(self, volume_type_id, **kwargs):
@@ -188,7 +194,7 @@
         post_body = json.dumps({'removeProjectAccess': kwargs})
         url = 'types/%s/action' % volume_type_id
         resp, body = self.post(url, post_body)
-        self.expected_success(202, resp.status)
+        self.validate_response(schema.remove_type_access, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def list_type_access(self, volume_type_id):
@@ -201,5 +207,5 @@
         url = 'types/%s/os-volume-type-access' % volume_type_id
         resp, body = self.get(url)
         body = json.loads(body)
-        self.expected_success(200, resp.status)
+        self.validate_response(schema.list_type_access, resp, body)
         return rest_client.ResponseBody(resp, body)
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index cb7acbf..99dd653 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -1008,13 +1008,18 @@
             port_id, ip4 = self._get_server_port_id_and_ip4(thing)
         else:
             ip4 = None
-        result = client.create_floatingip(
-            floating_network_id=external_network_id,
-            port_id=port_id,
-            tenant_id=thing['tenant_id'],
-            fixed_ip_address=ip4
-        )
+
+        kwargs = {
+            'floating_network_id': external_network_id,
+            'port_id': port_id,
+            'tenant_id': thing['tenant_id'],
+            'fixed_ip_address': ip4,
+        }
+        if CONF.network.subnet_id:
+            kwargs['subnet_id'] = CONF.network.subnet_id
+        result = client.create_floatingip(**kwargs)
         floating_ip = result['floatingip']
+
         self.addCleanup(test_utils.call_and_ignore_notfound_exc,
                         client.delete_floatingip,
                         floating_ip['id'])
diff --git a/tempest/scenario/test_network_basic_ops.py b/tempest/scenario/test_network_basic_ops.py
index f46c7e8..d8584ec 100644
--- a/tempest/scenario/test_network_basic_ops.py
+++ b/tempest/scenario/test_network_basic_ops.py
@@ -346,10 +346,19 @@
                 network_id=CONF.network.public_network_id)['subnets']
             if s['ip_version'] == 4
         ]
-        self.assertEqual(1, len(v4_subnets),
-                         "Found %d IPv4 subnets" % len(v4_subnets))
 
-        external_ips = [v4_subnets[0]['gateway_ip']]
+        if len(v4_subnets) > 1:
+            self.assertTrue(
+                CONF.network.subnet_id,
+                "Found %d subnets. Specify subnet using configuration "
+                "option [network].subnet_id."
+                % len(v4_subnets))
+            subnet = self.os_admin.subnets_client.show_subnet(
+                CONF.network.subnet_id)['subnet']
+            external_ips = [subnet['gateway_ip']]
+        else:
+            external_ips = [v4_subnets[0]['gateway_ip']]
+
         self._check_server_connectivity(self.floating_ip_tuple.floating_ip,
                                         external_ips)
 
diff --git a/tempest/scenario/test_volume_boot_pattern.py b/tempest/scenario/test_volume_boot_pattern.py
index ec44b13..0782389 100644
--- a/tempest/scenario/test_volume_boot_pattern.py
+++ b/tempest/scenario/test_volume_boot_pattern.py
@@ -167,6 +167,13 @@
         self.assertEqual(created_volume[0]['id'],
                          created_volume_info['attachments'][0]['volume_id'])
 
+        # Delete the server and wait
+        self._delete_server(server)
+
+        # Assert that the underlying volume is gone before class tearDown
+        # to prevent snapshot deletion from failing
+        self.volumes_client.wait_for_resource_deletion(created_volume[0]['id'])
+
     @decorators.idempotent_id('36c34c67-7b54-4b59-b188-02a2f458a63b')
     @testtools.skipUnless(CONF.volume_feature_enabled.snapshot,
                           'Cinder volume snapshots are disabled')
diff --git a/tempest/tests/cmd/test_cleanup_services.py b/tempest/tests/cmd/test_cleanup_services.py
index ae08d02..8366290 100644
--- a/tempest/tests/cmd/test_cleanup_services.py
+++ b/tempest/tests/cmd/test_cleanup_services.py
@@ -196,6 +196,7 @@
             is_save_state=is_save_state,
             is_preserve=is_preserve,
             is_dry_run=is_dry_run,
+            project_id='b8e3ece07bb049138d224436756e3b57',
             data={},
             saved_state_json=self.saved_state
             )
@@ -533,6 +534,135 @@
         self._test_saved_state_true([(self.get_method, self.response, 200)])
 
 
+class TestVolumeQuotaService(BaseCmdServiceTests):
+
+    service_class = 'VolumeQuotaService'
+    service_name = 'volume_quota_service'
+    response = {
+        "quota_set": {
+            "groups":
+                {"reserved": 0, "limit": 10, "in_use": 0},
+            "per_volume_gigabytes":
+                {"reserved": 0, "limit": -1, "in_use": 0},
+            "volumes":
+                {"reserved": 0, "limit": 10, "in_use": 0},
+            "gigabytes":
+                {"reserved": 0, "limit": 1000, "in_use": 0},
+            "backup_gigabytes":
+                {"reserved": 0, "limit": 1000, "in_use": 0},
+            "snapshots":
+                {"reserved": 0, "limit": 10, "in_use": 0},
+            "volumes_iscsi":
+                {"reserved": 0, "limit": -1, "in_use": 0},
+            "snapshots_iscsi":
+                {"reserved": 0, "limit": -1, "in_use": 0},
+            "backups":
+                {"reserved": 0, "limit": 10, "in_use": 0},
+            "gigabytes_iscsi":
+                {"reserved": 0, "limit": -1, "in_use": 0},
+            "id": "b8e3ece07bb049138d224436756e3b57"
+                }
+        }
+
+    def test_delete_fail(self):
+        delete_mock = [(self.delete_method, 'error', None),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock, fail=True)
+
+    def test_delete_pass(self):
+        delete_mock = [(self.delete_method, None, 200),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock)
+
+    def test_dry_run(self):
+        dry_mock = [(self.get_method, self.response, 200),
+                    (self.delete_method, "delete", None)]
+        self._test_dry_run_true(dry_mock)
+
+
+class TestNovaQuotaService(BaseCmdServiceTests):
+
+    service_class = 'NovaQuotaService'
+    service_name = 'nova_quota_service'
+    response = {
+        "limits": {
+            "rate": [],
+            "absolute": {
+                "maxServerMeta": 128,
+                "maxPersonality": 5,
+                "totalServerGroupsUsed": 0,
+                "maxImageMeta": 128,
+                "maxPersonalitySize": 10240,
+                "maxTotalKeypairs": 100,
+                "maxSecurityGroupRules": 20,
+                "maxServerGroups": 10,
+                "totalCoresUsed": 0,
+                "totalRAMUsed": 0,
+                "totalInstancesUsed": 0,
+                "maxSecurityGroups": 10,
+                "totalFloatingIpsUsed": 0,
+                "maxTotalCores": 20,
+                "maxServerGroupMembers": 10,
+                "maxTotalFloatingIps": 10,
+                "totalSecurityGroupsUsed": 0,
+                "maxTotalInstances": 10,
+                "maxTotalRAMSize": 51200
+                }
+            }
+        }
+
+    def test_delete_fail(self):
+        delete_mock = [(self.delete_method, 'error', None),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock, fail=True)
+
+    def test_delete_pass(self):
+        delete_mock = [(self.delete_method, None, 202),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock)
+
+    def test_dry_run(self):
+        dry_mock = [(self.get_method, self.response, 200),
+                    (self.delete_method, "delete", None)]
+        self._test_dry_run_true(dry_mock)
+
+
+class TestNetworkQuotaService(BaseCmdServiceTests):
+
+    service_class = 'NetworkQuotaService'
+    service_name = 'network_quota_service'
+    response = {
+        "quotas": [{
+            "subnet": 110,
+            "network": 100,
+            "floatingip": 50,
+            "tenant_id": "81e8490db559474dacb2212fca9cca2d",
+            "subnetpool": -1,
+            "security_group_rule": 100,
+            "trunk": -1,
+            "security_group": 10,
+            "router": 10,
+            "rbac_policy": 10, "project_id":
+            "81e8490db559474dacb2212fca9cca2d", "port": 500
+            }]
+    }
+
+    def test_delete_fail(self):
+        delete_mock = [(self.delete_method, 'error', None),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock, fail=True)
+
+    def test_delete_pass(self):
+        delete_mock = [(self.delete_method, None, 204),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock)
+
+    def test_dry_run(self):
+        dry_mock = [(self.get_method, self.response, 200),
+                    (self.delete_method, "delete", None)]
+        self._test_dry_run_true(dry_mock)
+
+
 # Begin network service classes
 class TestNetworkService(BaseCmdServiceTests):
 
diff --git a/tempest/tests/lib/services/identity/v3/test_application_credentials_client.py b/tempest/tests/lib/services/identity/v3/test_application_credentials_client.py
index 2774c44..8aed7d7 100644
--- a/tempest/tests/lib/services/identity/v3/test_application_credentials_client.py
+++ b/tempest/tests/lib/services/identity/v3/test_application_credentials_client.py
@@ -20,78 +20,116 @@
 class TestApplicationCredentialsClient(base.BaseServiceTest):
     FAKE_CREATE_APP_CRED = {
         "application_credential": {
-            "description": "fake application credential",
+            "name": "monitoring",
+            "secret": "rEaqvJka48mpv",
+            "description": "Application credential for monitoring.",
+            "expires_at": "2018-02-27T18:30:59Z",
             "roles": [
+                {"name": "Reader"}
+            ],
+            "access_rules": [
                 {
-                    "id": "c60fdd45",
-                    "domain_id": None,
-                    "name": "Member"
+                    "path": "/v2.0/metrics",
+                    "method": "GET",
+                    "service": "monitoring"
                 }
             ],
-            "expires_at": "2019-02-27T18:30:59.999999Z",
-            "secret": "_BVq0xU5L",
-            "unrestricted": None,
-            "project_id": "ddef321",
-            "id": "5499a186",
-            "name": "one"
+            "unrestricted": False
         }
     }
 
     FAKE_LIST_APP_CREDS = {
+        "links": {
+            "self": "http://example.com/identity/v3/users/" +
+                    "fd786d56402c4d1691372e7dee0d00b5/application_credentials",
+            "previous": None,
+            "next": None
+        },
         "application_credentials": [
             {
-                "description": "fake application credential",
+                "description": "Application credential for backups.",
                 "roles": [
                     {
                         "domain_id": None,
-                        "name": "Member",
-                        "id": "c60fdd45",
+                        "name": "Writer",
+                        "id": "6aff702516544aeca22817fd3bc39683"
                     }
                 ],
-                "expires_at": "2018-02-27T18:30:59.999999Z",
-                "unrestricted": None,
-                "project_id": "ddef321",
-                "id": "5499a186",
-                "name": "one"
+                "access_rules": [
+                ],
+                "links": {
+                    "self": "http://example.com/identity/v3/users/" +
+                            "fd786d56402c4d1691372e7dee0d00b5/" +
+                            "application_credentials/" +
+                            "308a7e905eee4071aac5971744c061f6"
+                },
+                "expires_at": "2018-02-27T18:30:59.000000",
+                "unrestricted": False,
+                "project_id": "231c62fb0fbd485b995e8b060c3f0d98",
+                "id": "308a7e905eee4071aac5971744c061f6",
+                "name": "backups"
             },
             {
-                "description": None,
+                "description": "Application credential for monitoring.",
                 "roles": [
                     {
-                        "id": "0f1837c8",
+                        "id": "6aff702516544aeca22817fd3bc39683",
                         "domain_id": None,
-                        "name": "anotherrole"
-                    },
-                    {
-                        "id": "c60fdd45",
-                        "domain_id": None,
-                        "name": "Member"
+                        "name": "Reader"
                     }
                 ],
-                "expires_at": None,
-                "unrestricted": None,
-                "project_id": "c5403d938",
-                "id": "d441c904f",
-                "name": "two"
+                "access_rules": [
+                    {
+                        "path": "/v2.0/metrics",
+                        "id": "07d719df00f349ef8de77d542edf010c",
+                        "service": "monitoring",
+                        "method": "GET"
+                    }
+                ],
+                "links": {
+                    "self": "http://example.com/identity/v3/users/" +
+                            "fd786d56402c4d1691372e7dee0d00b5/" +
+                            "application_credentials/" +
+                            "58d61ff8e6e34accb35874016d1dba8b"
+                },
+                "expires_at": "2018-02-27T18:30:59.000000",
+                "unrestricted": False,
+                "project_id": "231c62fb0fbd485b995e8b060c3f0d98",
+                "id": "58d61ff8e6e34accb35874016d1dba8b",
+                "name": "monitoring"
             }
         ]
     }
 
     FAKE_APP_CRED_INFO = {
         "application_credential": {
-            "description": None,
+            "description": "Application credential for monitoring.",
             "roles": [
                 {
+                    "id": "6aff702516544aeca22817fd3bc39683",
                     "domain_id": None,
-                    "name": "Member",
-                    "id": "c60fdd45",
+                    "name": "Reader"
                 }
             ],
-            "expires_at": None,
-            "unrestricted": None,
-            "project_id": "ddef321",
-            "id": "5499a186",
-            "name": "one"
+            "access_rules": [
+                {
+                    "path": "/v2.0/metrics",
+                    "id": "07d719df00f349ef8de77d542edf010c",
+                    "service": "monitoring",
+                    "method": "GET"
+                }
+            ],
+            "links": {
+                "self": "http://example.com/identity/v3/users/" +
+                        "fd786d56402c4d1691372e7dee0d00b5/" +
+                        "application_credentials/" +
+                        "58d61ff8e6e34accb35874016d1dba8b"
+            },
+            "expires_at": "2018-02-27T18:30:59.000000",
+            "unrestricted": False,
+            "project_id": "231c62fb0fbd485b995e8b060c3f0d98",
+            "id": "58d61ff8e6e34accb35874016d1dba8b",
+            "name": "monitoring"
         }
     }
 
@@ -118,7 +156,7 @@
             self.FAKE_APP_CRED_INFO,
             bytes_body,
             user_id="123456",
-            application_credential_id="5499a186")
+            application_credential_id="58d61ff8e6e34accb35874016d1dba8b")
 
     def _test_list_app_creds(self, bytes_body=False):
         self.check_service_client_function(
@@ -152,5 +190,5 @@
             'tempest.lib.common.rest_client.RestClient.delete',
             {},
             user_id="123456",
-            application_credential_id="5499a186",
+            application_credential_id="58d61ff8e6e34accb35874016d1dba8b",
             status=204)
diff --git a/tempest/tests/lib/services/image/v2/test_namespace_tags_client.py b/tempest/tests/lib/services/image/v2/test_namespace_tags_client.py
index 894e178..6b282f4 100644
--- a/tempest/tests/lib/services/image/v2/test_namespace_tags_client.py
+++ b/tempest/tests/lib/services/image/v2/test_namespace_tags_client.py
@@ -130,5 +130,5 @@
         self.check_service_client_function(
             self.client.delete_namespace_tags,
             'tempest.lib.common.rest_client.RestClient.delete',
-            {}, status=200,
+            {}, status=204,
             namespace="OS::Compute::Hypervisor")
diff --git a/tempest/tests/lib/services/volume/v3/test_attachments_client.py b/tempest/tests/lib/services/volume/v3/test_attachments_client.py
new file mode 100644
index 0000000..52c94e5
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v3/test_attachments_client.py
@@ -0,0 +1,46 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.lib.services.volume.v3 import attachments_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+from oslo_utils.fixture import uuidsentinel as uuids
+
+
+class TestAttachmentsClient(base.BaseServiceTest):
+
+    FAKE_ATTACHMENT_INFO = {
+        "attachment": {
+            "status": "attaching",
+            "detached_at": "2015-09-16T09:28:52.000000",
+            "connection_info": {},
+            "attached_at": "2015-09-16T09:28:52.000000",
+            "attach_mode": "ro",
+            "instance": uuids.instance_id,
+            "volume_id": uuids.volume_id,
+            "id": uuids.id,
+        }
+    }
+
+    def setUp(self):
+        super(TestAttachmentsClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = attachments_client.AttachmentsClient(fake_auth,
+                                                           'volume',
+                                                           'regionOne')
+
+    def test_show_attachment(self):
+        self.check_service_client_function(
+            self.client.show_attachment,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_ATTACHMENT_INFO, attachment_id=uuids.id)