Merge "Pause resource cleanup"
diff --git a/doc/source/conf.py b/doc/source/conf.py
index b9e22b5..23f732e 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -55,7 +55,7 @@
 extensions = ['sphinx.ext.autodoc',
               'sphinx.ext.todo',
               'sphinx.ext.viewcode',
-              'oslosphinx',
+              'openstackdocstheme',
               'oslo_config.sphinxconfiggen',
              ]
 
@@ -64,6 +64,14 @@
 
 todo_include_todos = True
 
+# openstackdocstheme options
+repository_name = 'openstack/tempest'
+bug_project = 'tempest'
+bug_tag = ''
+
+# Must set this variable to include year, month, day, hours, and minutes.
+html_last_updated_fmt = '%Y-%m-%d %H:%M'
+
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ['_templates']
 
@@ -127,7 +135,7 @@
 #html_theme_options = {}
 
 # Add any paths that contain custom themes here, relative to this directory.
-html_theme_path = [openstackdocstheme.get_html_theme_path()]
+#html_theme_path = []
 
 # The name for this set of Sphinx documents.  If None, it defaults to
 # "<project> v<release> documentation".
@@ -152,13 +160,6 @@
 
 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 # using the given strftime format.
-git_cmd = ["git", "log", "--pretty=format:'%ad, commit %h'", "--date=local",
-   "-n1"]
-try:
-    html_last_updated_fmt = subprocess.check_output(git_cmd).decode('utf-8')
-except Exception:
-    warnings.warn('Cannot get last updated time from git repository. '
-                  'Not setting "html_last_updated_fmt".')
 
 # If true, SmartyPants will be used to convert quotes and dashes to
 # typographically correct entities.
diff --git a/doc/source/microversion_testing.rst b/doc/source/microversion_testing.rst
index b516055..d6d90ba 100644
--- a/doc/source/microversion_testing.rst
+++ b/doc/source/microversion_testing.rst
@@ -326,6 +326,14 @@
 
  .. _2.42: http://docs.openstack.org/developer/nova/api_microversion_history.html#maximum-in-ocata
 
+ * `2.47`_
+
+ .. _2.47: http://docs.openstack.org/developer/nova/api_microversion_history.html#id42
+
+ * `2.48`_
+
+ .. _2.48: http://docs.openstack.org/developer/nova/api_microversion_history.html#id43
+
 * Volume
 
  * `3.3`_
diff --git a/releasenotes/notes/add-OAUTH-Token-Client-tempest-tests-6351eda451b95a86.yaml b/releasenotes/notes/add-OAUTH-Token-Client-tempest-tests-6351eda451b95a86.yaml
new file mode 100644
index 0000000..9115f03
--- /dev/null
+++ b/releasenotes/notes/add-OAUTH-Token-Client-tempest-tests-6351eda451b95a86.yaml
@@ -0,0 +1,3 @@
+---
+features:
+  - Add a new client to handle the OAUTH token feature from the identity API.
diff --git a/releasenotes/notes/add-params-to-identity-v3-list-endpoints-958a155be4e17e5b.yaml b/releasenotes/notes/add-params-to-identity-v3-list-endpoints-958a155be4e17e5b.yaml
new file mode 100644
index 0000000..46f3b49
--- /dev/null
+++ b/releasenotes/notes/add-params-to-identity-v3-list-endpoints-958a155be4e17e5b.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    The ``list_endpoints`` method of the v3 ``EndPointsClient`` class now has
+    an additional ``**params`` argument that enables passing additional
+    information in the query string of the HTTP request.
diff --git a/releasenotes/notes/add-server-diagnostics-validation-schema-b5a3c55b45aa718a.yaml b/releasenotes/notes/add-server-diagnostics-validation-schema-b5a3c55b45aa718a.yaml
new file mode 100644
index 0000000..e0ac87c
--- /dev/null
+++ b/releasenotes/notes/add-server-diagnostics-validation-schema-b5a3c55b45aa718a.yaml
@@ -0,0 +1,4 @@
+---
+features:
+  - |
+    Add validation schema for Nova server diagnostics API
\ No newline at end of file
diff --git a/releasenotes/notes/add-volume-groups-tempest-tests-dd7b2abfe2b48427.yaml b/releasenotes/notes/add-volume-groups-tempest-tests-dd7b2abfe2b48427.yaml
new file mode 100644
index 0000000..898d366
--- /dev/null
+++ b/releasenotes/notes/add-volume-groups-tempest-tests-dd7b2abfe2b48427.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    Add groups and group_types clients for the volume service as library.
+    Add tempest tests for create group, delete group, show group, and
+    list group volume APIs.
diff --git a/releasenotes/notes/move-volume-v3-base_client-to-volume-1edbz0f207c3b283.yaml b/releasenotes/notes/move-volume-v3-base_client-to-volume-1edbz0f207c3b283.yaml
new file mode 100644
index 0000000..ec81dc5
--- /dev/null
+++ b/releasenotes/notes/move-volume-v3-base_client-to-volume-1edbz0f207c3b283.yaml
@@ -0,0 +1,15 @@
+features:
+  - |
+    Move base_client from tempest.lib.services.volume.v3 to
+    tempest.lib.services.volume, so if we want to add new
+    interfaces based on a v2 client, we can make that v2
+    client inherit from volume.base_client.BaseClient to
+    get microversion support, and then to make the new v3
+    client inherit from the v2 client, thus to avoid the
+    multiple inheritance.
+deprecations:
+  - |
+    Deprecate class BaseClient from volume.v3.base_client
+    and move it to volume.base_client.
+    ``tempest.lib.services.volume.v3.base_client.BaseClient``
+    (new ``tempest.lib.services.volume.base_client.BaseClient``)
diff --git a/releasenotes/notes/set-cinder-api-v3-option-true-1b3e61e3129b7c00.yaml b/releasenotes/notes/set-cinder-api-v3-option-true-1b3e61e3129b7c00.yaml
new file mode 100644
index 0000000..6959ca7
--- /dev/null
+++ b/releasenotes/notes/set-cinder-api-v3-option-true-1b3e61e3129b7c00.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+  - |
+    The volume config option 'api_v3' default is changed to
+    ``True`` because the volume v3 API is CURRENT.
diff --git a/releasenotes/notes/tempest-workspace-delete-directory-feature-74d6d157a5a05561.yaml b/releasenotes/notes/tempest-workspace-delete-directory-feature-74d6d157a5a05561.yaml
new file mode 100644
index 0000000..ec21098
--- /dev/null
+++ b/releasenotes/notes/tempest-workspace-delete-directory-feature-74d6d157a5a05561.yaml
@@ -0,0 +1,5 @@
+---
+features:
+  - |
+    Added tempest workspace remove --name <workspace_name> --rmdir
+    feature to delete the workspace directory as well as entry.
diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py
index d240467..3137541 100644
--- a/releasenotes/source/conf.py
+++ b/releasenotes/source/conf.py
@@ -37,10 +37,18 @@
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 # ones.
 extensions = [
-    'oslosphinx',
+    'openstackdocstheme',
     'reno.sphinxext',
 ]
 
+# openstackdocstheme options
+repository_name = 'openstack/tempest'
+bug_project = 'tempest'
+bug_tag = ''
+
+# Must set this variable to include year, month, day, hours, and minutes.
+html_last_updated_fmt = '%Y-%m-%d %H:%M'
+
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ['_templates']
 
@@ -119,9 +127,6 @@
 # html_theme_options = {}
 
 # Add any paths that contain custom themes here, relative to this directory.
-import openstackdocstheme
-
-html_theme_path = [openstackdocstheme.get_html_theme_path()]
 
 # The name for this set of Sphinx documents.  If None, it defaults to
 # "<project> v<release> documentation".
diff --git a/tempest/api/compute/admin/test_auto_allocate_network.py b/tempest/api/compute/admin/test_auto_allocate_network.py
index ba8a214..83fe215 100644
--- a/tempest/api/compute/admin/test_auto_allocate_network.py
+++ b/tempest/api/compute/admin/test_auto_allocate_network.py
@@ -111,10 +111,12 @@
             LOG.info('(%s) Found more than one router for tenant.',
                      test_utils.find_test_caller())
 
-        # Let's just blindly remove any networks, duplicate or otherwise, that
-        # the test might have created even though Neutron will cleanup
-        # duplicate resources automatically (so ignore 404s).
-        networks = cls.networks_client.list_networks().get('networks', [])
+        # Remove any networks, duplicate or otherwise, that these tests
+        # created. All such networks will be in the current tenant. Neutron
+        # will cleanup duplicate resources automatically, so ignore 404s.
+        search_opts = {'tenant_id': cls.networks_client.tenant_id}
+        networks = cls.networks_client.list_networks(
+            **search_opts).get('networks', [])
 
         for router in routers:
             # Disassociate the subnets from the router. Because of the race
diff --git a/tempest/api/compute/admin/test_server_diagnostics.py b/tempest/api/compute/admin/test_server_diagnostics.py
new file mode 100644
index 0000000..005efdd
--- /dev/null
+++ b/tempest/api/compute/admin/test_server_diagnostics.py
@@ -0,0 +1,76 @@
+# Copyright 2017 Mirantis Inc.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import time
+
+from tempest.api.compute import base
+from tempest.lib import decorators
+
+
+class ServerDiagnosticsTest(base.BaseV2ComputeAdminTest):
+    min_microversion = None
+    max_microversion = '2.47'
+
+    @classmethod
+    def setup_clients(cls):
+        super(ServerDiagnosticsTest, cls).setup_clients()
+        cls.client = cls.os_admin.servers_client
+
+    @decorators.idempotent_id('31ff3486-b8a0-4f56-a6c0-aab460531db3')
+    def test_get_server_diagnostics(self):
+        server_id = self.create_test_server(wait_until='ACTIVE')['id']
+        diagnostics = self.client.show_server_diagnostics(server_id)
+
+        # NOTE(snikitin): Before microversion 2.48 response data from each
+        # hypervisor (libvirt, xen, vmware) was different. None of the fields
+        # were equal. As this test is common for libvirt, xen and vmware CI
+        # jobs we can't check any field in the response because all fields are
+        # different.
+        self.assertNotEmpty(diagnostics)
+
+
+class ServerDiagnosticsV248Test(base.BaseV2ComputeAdminTest):
+    min_microversion = '2.48'
+    max_microversion = 'latest'
+
+    @classmethod
+    def setup_clients(cls):
+        super(ServerDiagnosticsV248Test, cls).setup_clients()
+        cls.client = cls.os_admin.servers_client
+
+    @decorators.idempotent_id('64d0d48c-dff1-11e6-bf01-fe55135034f3')
+    def test_get_server_diagnostics(self):
+        server_id = self.create_test_server(wait_until='ACTIVE')['id']
+        # Response status and filed types will be checked by json schema
+        self.client.show_server_diagnostics(server_id)
+
+        # NOTE(snikitin): This is a special case for Xen hypervisor. In Xen
+        # case we're getting diagnostics stats from the RRDs which are updated
+        # every 5 seconds. It means that diagnostics information may be
+        # incomplete during first 5 seconds of VM life. In such cases methods
+        # which get diagnostics stats from Xen may raise exceptions or
+        # return `NaN` values. Such behavior must be handled correctly.
+        # Response must contain all diagnostics fields (may be with `None`
+        # values) and response status must be 200. Line above checks it by
+        # json schema.
+        time.sleep(10)
+        diagnostics = self.client.show_server_diagnostics(server_id)
+
+        # NOTE(snikitin): After 10 seconds diagnostics fields must contain
+        # not None values. But we will check only "memory_details.maximum"
+        # field because only this field meets all the following conditions:
+        # 1) This field may be unset because of Xen 5 seconds timeout.
+        # 2) This field is present in responses from all three supported
+        #    hypervisors (libvirt, xen, vmware).
+        self.assertIsNotNone(diagnostics['memory_details']['maximum'])
diff --git a/tempest/api/compute/admin/test_server_diagnostics_negative.py b/tempest/api/compute/admin/test_server_diagnostics_negative.py
new file mode 100644
index 0000000..d5b6674
--- /dev/null
+++ b/tempest/api/compute/admin/test_server_diagnostics_negative.py
@@ -0,0 +1,40 @@
+# Copyright 2017 Mirantis Inc.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.api.compute import base
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+
+class ServerDiagnosticsNegativeTest(base.BaseV2ComputeAdminTest):
+    min_microversion = None
+    max_microversion = '2.47'
+
+    @classmethod
+    def setup_clients(cls):
+        super(ServerDiagnosticsNegativeTest, cls).setup_clients()
+        cls.client = cls.servers_client
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('e84e2234-60d2-42fa-8b30-e2d3049724ac')
+    def test_get_server_diagnostics_by_non_admin(self):
+        # Non-admin user cannot view server diagnostics according to policy
+        server_id = self.create_test_server(wait_until='ACTIVE')['id']
+        self.assertRaises(lib_exc.Forbidden,
+                          self.client.show_server_diagnostics, server_id)
+
+
+class ServerDiagnosticsNegativeV248Test(ServerDiagnosticsNegativeTest):
+    min_microversion = '2.48'
+    max_microversion = 'latest'
diff --git a/tempest/api/compute/admin/test_servers.py b/tempest/api/compute/admin/test_servers.py
index 789049b..0521cca 100644
--- a/tempest/api/compute/admin/test_servers.py
+++ b/tempest/api/compute/admin/test_servers.py
@@ -164,17 +164,6 @@
         server = self.client.show_server(self.s1_id)['server']
         self.assertEqual(server['status'], 'ACTIVE')
 
-    @decorators.skip_because(bug="1240043")
-    @decorators.idempotent_id('31ff3486-b8a0-4f56-a6c0-aab460531db3')
-    def test_get_server_diagnostics_by_admin(self):
-        # Retrieve server diagnostics by admin user
-        diagnostic = self.client.show_server_diagnostics(self.s1_id)
-        basic_attrs = ['rx_packets', 'rx_errors', 'rx_drop',
-                       'tx_packets', 'tx_errors', 'tx_drop',
-                       'read_req', 'write_req', 'cpu', 'memory']
-        for key in basic_attrs:
-            self.assertIn(key, str(diagnostic.keys()))
-
     @decorators.idempotent_id('682cb127-e5bb-4f53-87ce-cb9003604442')
     def test_rebuild_server_in_error_state(self):
         # The server in error state should be rebuilt using the provided
diff --git a/tempest/api/compute/admin/test_servers_negative.py b/tempest/api/compute/admin/test_servers_negative.py
index ca53696..9023759 100644
--- a/tempest/api/compute/admin/test_servers_negative.py
+++ b/tempest/api/compute/admin/test_servers_negative.py
@@ -108,14 +108,6 @@
                           self.client.reset_state, '999', state='error')
 
     @decorators.attr(type=['negative'])
-    @decorators.idempotent_id('e84e2234-60d2-42fa-8b30-e2d3049724ac')
-    def test_get_server_diagnostics_by_non_admin(self):
-        # Non-admin user can not view server diagnostics according to policy
-        self.assertRaises(lib_exc.Forbidden,
-                          self.non_adm_client.show_server_diagnostics,
-                          self.s1_id)
-
-    @decorators.attr(type=['negative'])
     @decorators.idempotent_id('46a4e1ca-87ae-4d28-987a-1b6b136a0221')
     def test_migrate_non_existent_server(self):
         # migrate a non existent server
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index d893446..429ded5 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -22,6 +22,7 @@
 from tempest.common import waiters
 from tempest import config
 from tempest import exceptions
+from tempest.lib.common import api_version_request
 from tempest.lib.common import api_version_utils
 from tempest.lib.common.utils import data_utils
 from tempest.lib.common.utils import test_utils
@@ -202,6 +203,15 @@
         """
         if 'name' not in kwargs:
             kwargs['name'] = data_utils.rand_name(cls.__name__ + "-server")
+
+        request_version = api_version_request.APIVersionRequest(
+            cls.request_microversion)
+        v2_37_version = api_version_request.APIVersionRequest('2.37')
+
+        # NOTE(snikitin): since microversion v2.37 'networks' field is required
+        if request_version >= v2_37_version and 'networks' not in kwargs:
+            kwargs['networks'] = 'none'
+
         tenant_network = cls.get_tenant_network()
         body, servers = compute.create_test_server(
             cls.os_primary,
diff --git a/tempest/api/compute/flavors/test_flavors.py b/tempest/api/compute/flavors/test_flavors.py
index bf4c958..d5bb45a 100644
--- a/tempest/api/compute/flavors/test_flavors.py
+++ b/tempest/api/compute/flavors/test_flavors.py
@@ -68,7 +68,7 @@
 
         params = {'marker': flavor_id}
         flavors = self.flavors_client.list_flavors(**params)['flavors']
-        self.assertFalse(any([i for i in flavors if i['id'] == flavor_id]),
+        self.assertEmpty([i for i in flavors if i['id'] == flavor_id],
                          'The list of flavors did not start after the marker.')
 
     @decorators.idempotent_id('6db2f0c0-ddee-4162-9c84-0703d3dd1107')
@@ -80,7 +80,7 @@
         params = {'marker': flavor_id}
         flavors = self.flavors_client.list_flavors(detail=True,
                                                    **params)['flavors']
-        self.assertFalse(any([i for i in flavors if i['id'] == flavor_id]),
+        self.assertEmpty([i for i in flavors if i['id'] == flavor_id],
                          'The list of flavors did not start after the marker.')
 
     @decorators.idempotent_id('3df2743e-3034-4e57-a4cb-b6527f6eac79')
@@ -92,7 +92,7 @@
         params = {self._min_disk: flavor['disk'] + 1}
         flavors = self.flavors_client.list_flavors(detail=True,
                                                    **params)['flavors']
-        self.assertFalse(any([i for i in flavors if i['id'] == flavor_id]))
+        self.assertEmpty([i for i in flavors if i['id'] == flavor_id])
 
     @decorators.idempotent_id('09fe7509-b4ee-4b34-bf8b-39532dc47292')
     def test_list_flavors_detailed_filter_by_min_ram(self):
@@ -103,7 +103,7 @@
         params = {self._min_ram: flavor['ram'] + 1}
         flavors = self.flavors_client.list_flavors(detail=True,
                                                    **params)['flavors']
-        self.assertFalse(any([i for i in flavors if i['id'] == flavor_id]))
+        self.assertEmpty([i for i in flavors if i['id'] == flavor_id])
 
     @decorators.idempotent_id('10645a4d-96f5-443f-831b-730711e11dd4')
     def test_list_flavors_filter_by_min_disk(self):
@@ -113,7 +113,7 @@
 
         params = {self._min_disk: flavor['disk'] + 1}
         flavors = self.flavors_client.list_flavors(**params)['flavors']
-        self.assertFalse(any([i for i in flavors if i['id'] == flavor_id]))
+        self.assertEmpty([i for i in flavors if i['id'] == flavor_id])
 
     @decorators.idempotent_id('935cf550-e7c8-4da6-8002-00f92d5edfaa')
     def test_list_flavors_filter_by_min_ram(self):
@@ -123,4 +123,4 @@
 
         params = {self._min_ram: flavor['ram'] + 1}
         flavors = self.flavors_client.list_flavors(**params)['flavors']
-        self.assertFalse(any([i for i in flavors if i['id'] == flavor_id]))
+        self.assertEmpty([i for i in flavors if i['id'] == flavor_id])
diff --git a/tempest/api/compute/images/test_list_image_filters.py b/tempest/api/compute/images/test_list_image_filters.py
index 6677aa2..acc8b3e 100644
--- a/tempest/api/compute/images/test_list_image_filters.py
+++ b/tempest/api/compute/images/test_list_image_filters.py
@@ -129,9 +129,9 @@
         params = {'status': 'ACTIVE'}
         images = self.client.list_images(**params)['images']
 
-        self.assertTrue(any([i for i in images if i['id'] == self.image1_id]))
-        self.assertTrue(any([i for i in images if i['id'] == self.image2_id]))
-        self.assertTrue(any([i for i in images if i['id'] == self.image3_id]))
+        self.assertNotEmpty([i for i in images if i['id'] == self.image1_id])
+        self.assertNotEmpty([i for i in images if i['id'] == self.image2_id])
+        self.assertNotEmpty([i for i in images if i['id'] == self.image3_id])
 
     @decorators.idempotent_id('33163b73-79f5-4d07-a7ea-9213bcc468ff')
     def test_list_images_filter_by_name(self):
@@ -140,9 +140,9 @@
         params = {'name': self.image1['name']}
         images = self.client.list_images(**params)['images']
 
-        self.assertTrue(any([i for i in images if i['id'] == self.image1_id]))
-        self.assertFalse(any([i for i in images if i['id'] == self.image2_id]))
-        self.assertFalse(any([i for i in images if i['id'] == self.image3_id]))
+        self.assertNotEmpty([i for i in images if i['id'] == self.image1_id])
+        self.assertEmpty([i for i in images if i['id'] == self.image2_id])
+        self.assertEmpty([i for i in images if i['id'] == self.image3_id])
 
     @decorators.idempotent_id('9f238683-c763-45aa-b848-232ec3ce3105')
     @testtools.skipUnless(CONF.compute_feature_enabled.snapshot,
@@ -152,14 +152,13 @@
         params = {'server': self.server1['id']}
         images = self.client.list_images(**params)['images']
 
-        self.assertTrue(any([i for i in images
-                             if i['id'] == self.snapshot1_id]),
-                        "Failed to find image %s in images. Got images %s" %
-                        (self.image1_id, images))
-        self.assertTrue(any([i for i in images
-                             if i['id'] == self.snapshot2_id]))
-        self.assertFalse(any([i for i in images
-                              if i['id'] == self.snapshot3_id]))
+        self.assertNotEmpty([i for i in images
+                             if i['id'] == self.snapshot1_id],
+                            "Failed to find image %s in images. "
+                            "Got images %s" % (self.image1_id, images))
+        self.assertNotEmpty([i for i in images
+                             if i['id'] == self.snapshot2_id])
+        self.assertEmpty([i for i in images if i['id'] == self.snapshot3_id])
 
     @decorators.idempotent_id('05a377b8-28cf-4734-a1e6-2ab5c38bf606')
     @testtools.skipUnless(CONF.compute_feature_enabled.snapshot,
@@ -173,12 +172,12 @@
             params = {'server': link['href']}
             images = self.client.list_images(**params)['images']
 
-            self.assertFalse(any([i for i in images
-                                  if i['id'] == self.snapshot1_id]))
-            self.assertFalse(any([i for i in images
-                                  if i['id'] == self.snapshot2_id]))
-            self.assertTrue(any([i for i in images
-                                 if i['id'] == self.snapshot3_id]))
+            self.assertEmpty([i for i in images
+                              if i['id'] == self.snapshot1_id])
+            self.assertEmpty([i for i in images
+                              if i['id'] == self.snapshot2_id])
+            self.assertNotEmpty([i for i in images
+                                 if i['id'] == self.snapshot3_id])
 
     @decorators.idempotent_id('e3356918-4d3e-4756-81d5-abc4524ba29f')
     @testtools.skipUnless(CONF.compute_feature_enabled.snapshot,
@@ -188,14 +187,13 @@
         params = {'type': 'snapshot'}
         images = self.client.list_images(**params)['images']
 
-        self.assertTrue(any([i for i in images
-                             if i['id'] == self.snapshot1_id]))
-        self.assertTrue(any([i for i in images
-                             if i['id'] == self.snapshot2_id]))
-        self.assertTrue(any([i for i in images
-                             if i['id'] == self.snapshot3_id]))
-        self.assertFalse(any([i for i in images
-                              if i['id'] == self.image_ref]))
+        self.assertNotEmpty([i for i in images
+                             if i['id'] == self.snapshot1_id])
+        self.assertNotEmpty([i for i in images
+                             if i['id'] == self.snapshot2_id])
+        self.assertNotEmpty([i for i in images
+                             if i['id'] == self.snapshot3_id])
+        self.assertEmpty([i for i in images if i['id'] == self.image_ref])
 
     @decorators.idempotent_id('3a484ca9-67ba-451e-b494-7fcf28d32d62')
     def test_list_images_limit_results(self):
@@ -212,8 +210,8 @@
         # Filter by the image's created time
         params = {'changes-since': self.image3['created']}
         images = self.client.list_images(**params)['images']
-        found = any([i for i in images if i['id'] == self.image3_id])
-        self.assertTrue(found)
+        found = [i for i in images if i['id'] == self.image3_id]
+        self.assertNotEmpty(found)
 
     @decorators.idempotent_id('9b0ea018-6185-4f71-948a-a123a107988e')
     def test_list_images_with_detail_filter_by_status(self):
@@ -222,9 +220,9 @@
         params = {'status': 'ACTIVE'}
         images = self.client.list_images(detail=True, **params)['images']
 
-        self.assertTrue(any([i for i in images if i['id'] == self.image1_id]))
-        self.assertTrue(any([i for i in images if i['id'] == self.image2_id]))
-        self.assertTrue(any([i for i in images if i['id'] == self.image3_id]))
+        self.assertNotEmpty([i for i in images if i['id'] == self.image1_id])
+        self.assertNotEmpty([i for i in images if i['id'] == self.image2_id])
+        self.assertNotEmpty([i for i in images if i['id'] == self.image3_id])
 
     @decorators.idempotent_id('644ea267-9bd9-4f3b-af9f-dffa02396a17')
     def test_list_images_with_detail_filter_by_name(self):
@@ -233,9 +231,9 @@
         params = {'name': self.image1['name']}
         images = self.client.list_images(detail=True, **params)['images']
 
-        self.assertTrue(any([i for i in images if i['id'] == self.image1_id]))
-        self.assertFalse(any([i for i in images if i['id'] == self.image2_id]))
-        self.assertFalse(any([i for i in images if i['id'] == self.image3_id]))
+        self.assertNotEmpty([i for i in images if i['id'] == self.image1_id])
+        self.assertEmpty([i for i in images if i['id'] == self.image2_id])
+        self.assertEmpty([i for i in images if i['id'] == self.image3_id])
 
     @decorators.idempotent_id('ba2fa9a9-b672-47cc-b354-3b4c0600e2cb')
     def test_list_images_with_detail_limit_results(self):
@@ -257,12 +255,12 @@
             params = {'server': link['href']}
             images = self.client.list_images(detail=True, **params)['images']
 
-            self.assertFalse(any([i for i in images
-                                  if i['id'] == self.snapshot1_id]))
-            self.assertFalse(any([i for i in images
-                                  if i['id'] == self.snapshot2_id]))
-            self.assertTrue(any([i for i in images
-                                 if i['id'] == self.snapshot3_id]))
+            self.assertEmpty([i for i in images
+                              if i['id'] == self.snapshot1_id])
+            self.assertEmpty([i for i in images
+                              if i['id'] == self.snapshot2_id])
+            self.assertNotEmpty([i for i in images
+                                 if i['id'] == self.snapshot3_id])
 
     @decorators.idempotent_id('888c0cc0-7223-43c5-9db0-b125fd0a393b')
     @testtools.skipUnless(CONF.compute_feature_enabled.snapshot,
@@ -273,14 +271,13 @@
         images = self.client.list_images(detail=True, **params)['images']
         self.client.show_image(self.image_ref)
 
-        self.assertTrue(any([i for i in images
-                             if i['id'] == self.snapshot1_id]))
-        self.assertTrue(any([i for i in images
-                             if i['id'] == self.snapshot2_id]))
-        self.assertTrue(any([i for i in images
-                             if i['id'] == self.snapshot3_id]))
-        self.assertFalse(any([i for i in images
-                              if i['id'] == self.image_ref]))
+        self.assertNotEmpty([i for i in images
+                             if i['id'] == self.snapshot1_id])
+        self.assertNotEmpty([i for i in images
+                             if i['id'] == self.snapshot2_id])
+        self.assertNotEmpty([i for i in images
+                             if i['id'] == self.snapshot3_id])
+        self.assertEmpty([i for i in images if i['id'] == self.image_ref])
 
     @decorators.idempotent_id('7d439e18-ac2e-4827-b049-7e18004712c4')
     def test_list_images_with_detail_filter_by_changes_since(self):
@@ -290,4 +287,4 @@
         # Filter by the image's created time
         params = {'changes-since': self.image1['created']}
         images = self.client.list_images(detail=True, **params)['images']
-        self.assertTrue(any([i for i in images if i['id'] == self.image1_id]))
+        self.assertNotEmpty([i for i in images if i['id'] == self.image1_id])
diff --git a/tempest/api/compute/images/test_list_images.py b/tempest/api/compute/images/test_list_images.py
index 5d3cbf3..e2dbd72 100644
--- a/tempest/api/compute/images/test_list_images.py
+++ b/tempest/api/compute/images/test_list_images.py
@@ -44,12 +44,12 @@
     def test_list_images(self):
         # The list of all images should contain the image
         images = self.client.list_images()['images']
-        found = any([i for i in images if i['id'] == self.image_ref])
-        self.assertTrue(found)
+        found = [i for i in images if i['id'] == self.image_ref]
+        self.assertNotEmpty(found)
 
     @decorators.idempotent_id('9f94cb6b-7f10-48c5-b911-a0b84d7d4cd6')
     def test_list_images_with_detail(self):
         # Detailed list of all images should contain the expected images
         images = self.client.list_images(detail=True)['images']
-        found = any([i for i in images if i['id'] == self.image_ref])
-        self.assertTrue(found)
+        found = [i for i in images if i['id'] == self.image_ref]
+        self.assertNotEmpty(found)
diff --git a/tempest/api/compute/security_groups/test_security_group_rules.py b/tempest/api/compute/security_groups/test_security_group_rules.py
index b568f7d..124db0e 100644
--- a/tempest/api/compute/security_groups/test_security_group_rules.py
+++ b/tempest/api/compute/security_groups/test_security_group_rules.py
@@ -159,8 +159,8 @@
         # Get rules of the created Security Group
         rules = self.security_groups_client.show_security_group(
             securitygroup_id)['security_group']['rules']
-        self.assertTrue(any([i for i in rules if i['id'] == rule1_id]))
-        self.assertTrue(any([i for i in rules if i['id'] == rule2_id]))
+        self.assertNotEmpty([i for i in rules if i['id'] == rule1_id])
+        self.assertNotEmpty([i for i in rules if i['id'] == rule2_id])
 
     @decorators.idempotent_id('fc5c5acf-2091-43a6-a6ae-e42760e9ffaf')
     @test.services('network')
diff --git a/tempest/api/compute/servers/test_create_server.py b/tempest/api/compute/servers/test_create_server.py
index db42c6c..ffadd96 100644
--- a/tempest/api/compute/servers/test_create_server.py
+++ b/tempest/api/compute/servers/test_create_server.py
@@ -97,16 +97,16 @@
         # The created server should be in the list of all servers
         body = self.client.list_servers()
         servers = body['servers']
-        found = any([i for i in servers if i['id'] == self.server['id']])
-        self.assertTrue(found)
+        found = [i for i in servers if i['id'] == self.server['id']]
+        self.assertNotEmpty(found)
 
     @decorators.idempotent_id('585e934c-448e-43c4-acbf-d06a9b899997')
     def test_list_servers_with_detail(self):
         # The created server should be in the detailed list of all servers
         body = self.client.list_servers(detail=True)
         servers = body['servers']
-        found = any([i for i in servers if i['id'] == self.server['id']])
-        self.assertTrue(found)
+        found = [i for i in servers if i['id'] == self.server['id']]
+        self.assertNotEmpty(found)
 
     @decorators.idempotent_id('cbc0f52f-05aa-492b-bdc1-84b575ca294b')
     @testtools.skipUnless(CONF.validation.run_validation,
diff --git a/tempest/api/compute/servers/test_device_tagging.py b/tempest/api/compute/servers/test_device_tagging.py
index 9ab508d..7ee1b02 100644
--- a/tempest/api/compute/servers/test_device_tagging.py
+++ b/tempest/api/compute/servers/test_device_tagging.py
@@ -84,10 +84,14 @@
                 if d['mac'] == self.net_2_200_mac:
                     self.assertEqual(d['tags'], ['net-2-200'])
 
-        found_devices = [d['tags'][0] for d in md_dict['devices']]
-        self.assertItemsEqual(found_devices, ['port-1', 'port-2', 'net-1',
-                                              'net-2-100', 'net-2-200',
-                                              'boot', 'other'])
+            # A hypervisor may present multiple paths to a tagged disk, so
+            # there may be duplicated tags in the metadata, use set() to
+            # remove duplicated tags.
+            found_devices = [d['tags'][0] for d in md_dict['devices']]
+            self.assertEqual(set(found_devices), set(['port-1', 'port-2',
+                                                      'net-1', 'net-2-100',
+                                                      'net-2-200', 'boot',
+                                                      'other']))
 
     @decorators.idempotent_id('a2e65a6c-66f1-4442-aaa8-498c31778d96')
     @test.services('network', 'volume', 'image')
diff --git a/tempest/api/compute/servers/test_server_actions.py b/tempest/api/compute/servers/test_server_actions.py
index 6f072b2..cd09177 100644
--- a/tempest/api/compute/servers/test_server_actions.py
+++ b/tempest/api/compute/servers/test_server_actions.py
@@ -517,23 +517,33 @@
     @decorators.idempotent_id('77eba8e0-036e-4635-944b-f7a8f3b78dc9')
     @testtools.skipUnless(CONF.compute_feature_enabled.shelve,
                           'Shelve is not available.')
+    @test.services('image')
     def test_shelve_unshelve_server(self):
+        if CONF.image_feature_enabled.api_v2:
+            glance_client = self.os_primary.image_client_v2
+        elif CONF.image_feature_enabled.api_v1:
+            glance_client = self.os_primary.image_client
+        else:
+            raise lib_exc.InvalidConfiguration(
+                'Either api_v1 or api_v2 must be True in '
+                '[image-feature-enabled].')
         compute.shelve_server(self.client, self.server_id,
                               force_shelve_offload=True)
 
         server = self.client.show_server(self.server_id)['server']
         image_name = server['name'] + '-shelved'
         params = {'name': image_name}
-        images = self.compute_images_client.list_images(**params)['images']
+        if CONF.image_feature_enabled.api_v2:
+            images = glance_client.list_images(params)['images']
+        elif CONF.image_feature_enabled.api_v1:
+            images = glance_client.list_images(
+                detail=True, **params)['images']
         self.assertEqual(1, len(images))
         self.assertEqual(image_name, images[0]['name'])
 
         self.client.unshelve_server(self.server_id)
         waiters.wait_for_server_status(self.client, self.server_id, 'ACTIVE')
-
-        images = self.compute_images_client.list_images(**params)['images']
-        msg = ('After unshelve, shelved image is not deleted.')
-        self.assertEmpty(images, msg)
+        glance_client.wait_for_resource_deletion(images[0]['id'])
 
     @decorators.idempotent_id('af8eafd4-38a7-4a4b-bdbc-75145a580560')
     def test_stop_start_server(self):
diff --git a/tempest/api/compute/servers/test_servers.py b/tempest/api/compute/servers/test_servers.py
index ea4c141..7fd1dd1 100644
--- a/tempest/api/compute/servers/test_servers.py
+++ b/tempest/api/compute/servers/test_servers.py
@@ -134,3 +134,14 @@
         waiters.wait_for_server_status(self.client, server['id'], 'ACTIVE')
         server = self.client.show_server(server['id'])['server']
         self.assertEqual('2001:2001::3', server['accessIPv6'])
+
+
+class ServerShowV247Test(base.BaseV2ComputeTest):
+    min_microversion = '2.47'
+    max_microversion = 'latest'
+
+    @decorators.idempotent_id('88b0bdb2-494c-11e7-a919-92ebcb67fe33')
+    def test_show_server(self):
+        server = self.create_test_server()
+        # All fields will be checked by API schema
+        self.servers_client.show_server(server['id'])
diff --git a/tempest/api/identity/admin/v2/test_roles.py b/tempest/api/identity/admin/v2/test_roles.py
index 479663c..124bb5f 100644
--- a/tempest/api/identity/admin/v2/test_roles.py
+++ b/tempest/api/identity/admin/v2/test_roles.py
@@ -54,7 +54,7 @@
         """Return a list of all roles."""
         body = self.roles_client.list_roles()['roles']
         found = [role for role in body if role in self.roles]
-        self.assertTrue(any(found))
+        self.assertNotEmpty(found)
         self.assertEqual(len(found), len(self.roles))
 
     @decorators.idempotent_id('c62d909d-6c21-48c0-ae40-0a0760e6db5e')
@@ -68,13 +68,13 @@
 
         body = self.roles_client.list_roles()['roles']
         found = [role for role in body if role['name'] == role_name]
-        self.assertTrue(any(found))
+        self.assertNotEmpty(found)
 
         body = self.roles_client.delete_role(found[0]['id'])
 
         body = self.roles_client.list_roles()['roles']
         found = [role for role in body if role['name'] == role_name]
-        self.assertFalse(any(found))
+        self.assertEmpty(found)
 
     @decorators.idempotent_id('db6870bd-a6ed-43be-a9b1-2f10a5c9994f')
     def test_get_role_by_id(self):
diff --git a/tempest/api/identity/admin/v2/test_tenants.py b/tempest/api/identity/admin/v2/test_tenants.py
index ad9b983..0f955bf 100644
--- a/tempest/api/identity/admin/v2/test_tenants.py
+++ b/tempest/api/identity/admin/v2/test_tenants.py
@@ -37,7 +37,7 @@
 
         body = self.tenants_client.list_tenants()['tenants']
         found = [tenant for tenant in body if tenant['id'] in tenant_ids]
-        self.assertFalse(any(found), 'Tenants failed to delete')
+        self.assertEmpty(found, 'Tenants failed to delete')
 
     @decorators.idempotent_id('d25e9f24-1310-4d29-b61b-d91299c21d6d')
     def test_tenant_create_with_description(self):
diff --git a/tempest/api/identity/admin/v3/test_endpoints.py b/tempest/api/identity/admin/v3/test_endpoints.py
index b1ae2aa..c9faa9a 100644
--- a/tempest/api/identity/admin/v3/test_endpoints.py
+++ b/tempest/api/identity/admin/v3/test_endpoints.py
@@ -29,56 +29,93 @@
     def resource_setup(cls):
         super(EndPointsTestJSON, cls).resource_setup()
         cls.service_ids = list()
-        s_name = data_utils.rand_name('service')
-        s_type = data_utils.rand_name('type')
-        s_description = data_utils.rand_name('description')
+
+        # Create endpoints so as to use for LIST and GET test cases
+        interfaces = ['public', 'internal']
+        cls.setup_endpoint_ids = list()
+        for i in range(2):
+            cls._create_service()
+            region = data_utils.rand_name('region')
+            url = data_utils.rand_url()
+            endpoint = cls.client.create_endpoint(
+                service_id=cls.service_ids[i], interface=interfaces[i],
+                url=url, region=region, enabled=True)['endpoint']
+            cls.setup_endpoint_ids.append(endpoint['id'])
+
+    @classmethod
+    def _create_service(cls, s_name=None, s_type=None, s_description=None):
+        if s_name is None:
+            s_name = data_utils.rand_name('service')
+        if s_type is None:
+            s_type = data_utils.rand_name('type')
+        if s_description is None:
+            s_description = data_utils.rand_name('description')
         service_data = (
             cls.services_client.create_service(name=s_name, type=s_type,
                                                description=s_description))
-        cls.service_id = service_data['service']['id']
-        cls.service_ids.append(cls.service_id)
-        # Create endpoints so as to use for LIST and GET test cases
-        cls.setup_endpoints = list()
-        for _ in range(2):
-            region = data_utils.rand_name('region')
-            url = data_utils.rand_url()
-            interface = 'public'
-            endpoint = cls.client.create_endpoint(service_id=cls.service_id,
-                                                  interface=interface,
-                                                  url=url, region=region,
-                                                  enabled=True)['endpoint']
-            cls.setup_endpoints.append(endpoint)
+        service = service_data['service']
+        cls.service_ids.append(service['id'])
+        return service
 
     @classmethod
     def resource_cleanup(cls):
-        for e in cls.setup_endpoints:
-            cls.client.delete_endpoint(e['id'])
+        for e in cls.setup_endpoint_ids:
+            cls.client.delete_endpoint(e)
         for s in cls.service_ids:
             cls.services_client.delete_service(s)
         super(EndPointsTestJSON, cls).resource_cleanup()
 
     @decorators.idempotent_id('c19ecf90-240e-4e23-9966-21cee3f6a618')
     def test_list_endpoints(self):
-        # Get a list of endpoints
+        # Get the list of all the endpoints.
         fetched_endpoints = self.client.list_endpoints()['endpoints']
-        # Asserting LIST endpoints
+        fetched_endpoint_ids = [e['id'] for e in fetched_endpoints]
+        # Check that all the created endpoints are present in
+        # "fetched_endpoints".
         missing_endpoints =\
-            [e for e in self.setup_endpoints if e not in fetched_endpoints]
-        self.assertEmpty(missing_endpoints,
+            [e for e in self.setup_endpoint_ids
+             if e not in fetched_endpoint_ids]
+        self.assertEqual(0, len(missing_endpoints),
                          "Failed to find endpoint %s in fetched list" %
                          ', '.join(str(e) for e in missing_endpoints))
 
+        # Check that filtering endpoints by service_id works.
+        fetched_endpoints_for_service = self.client.list_endpoints(
+            service_id=self.service_ids[0])['endpoints']
+        fetched_endpoints_for_alt_service = self.client.list_endpoints(
+            service_id=self.service_ids[1])['endpoints']
+
+        # Assert that both filters returned the correct result.
+        self.assertEqual(1, len(fetched_endpoints_for_service))
+        self.assertEqual(1, len(fetched_endpoints_for_alt_service))
+        self.assertEqual(set(self.setup_endpoint_ids),
+                         set([fetched_endpoints_for_service[0]['id'],
+                              fetched_endpoints_for_alt_service[0]['id']]))
+
+        # Check that filtering endpoints by interface works.
+        fetched_public_endpoints = self.client.list_endpoints(
+            interface='public')['endpoints']
+        fetched_internal_endpoints = self.client.list_endpoints(
+            interface='internal')['endpoints']
+
+        # Check that the expected endpoint_id is present per filter. [0] is
+        # public and [1] is internal.
+        self.assertIn(self.setup_endpoint_ids[0],
+                      [e['id'] for e in fetched_public_endpoints])
+        self.assertIn(self.setup_endpoint_ids[1],
+                      [e['id'] for e in fetched_internal_endpoints])
+
     @decorators.idempotent_id('0e2446d2-c1fd-461b-a729-b9e73e3e3b37')
     def test_create_list_show_delete_endpoint(self):
         region = data_utils.rand_name('region')
         url = data_utils.rand_url()
         interface = 'public'
-        endpoint = self.client.create_endpoint(service_id=self.service_id,
+        endpoint = self.client.create_endpoint(service_id=self.service_ids[0],
                                                interface=interface,
                                                url=url, region=region,
                                                enabled=True)['endpoint']
 
-        self.setup_endpoints.append(endpoint)
+        self.setup_endpoint_ids.append(endpoint['id'])
         # Asserting Create Endpoint response body
         self.assertIn('id', endpoint)
         self.assertEqual(region, endpoint['region'])
@@ -93,7 +130,7 @@
         fetched_endpoint = (
             self.client.show_endpoint(endpoint['id'])['endpoint'])
         # Asserting if the attributes of endpoint are the same
-        self.assertEqual(self.service_id, fetched_endpoint['service_id'])
+        self.assertEqual(self.service_ids[0], fetched_endpoint['service_id'])
         self.assertEqual(interface, fetched_endpoint['interface'])
         self.assertEqual(url, fetched_endpoint['url'])
         self.assertEqual(region, fetched_endpoint['region'])
@@ -101,7 +138,7 @@
 
         # Deleting the endpoint created in this method
         self.client.delete_endpoint(endpoint['id'])
-        self.setup_endpoints.remove(endpoint)
+        self.setup_endpoint_ids.remove(endpoint['id'])
 
         # Checking whether endpoint is deleted successfully
         fetched_endpoints = self.client.list_endpoints()['endpoints']
@@ -117,7 +154,7 @@
         url1 = data_utils.rand_url()
         interface1 = 'public'
         endpoint_for_update = (
-            self.client.create_endpoint(service_id=self.service_id,
+            self.client.create_endpoint(service_id=self.service_ids[0],
                                         interface=interface1,
                                         url=url1, region=region1,
                                         enabled=True)['endpoint'])
@@ -126,11 +163,8 @@
         s_name = data_utils.rand_name('service')
         s_type = data_utils.rand_name('type')
         s_description = data_utils.rand_name('description')
-        service2 = (
-            self.services_client.create_service(name=s_name, type=s_type,
-                                                description=s_description))
-        service2 = service2['service']
-        self.service_ids.append(service2['id'])
+        service2 = self._create_service(s_name=s_name, s_type=s_type,
+                                        s_description=s_description)
         # Updating endpoint with new values
         region2 = data_utils.rand_name('region')
         url2 = data_utils.rand_url()
diff --git a/tempest/api/identity/admin/v3/test_services.py b/tempest/api/identity/admin/v3/test_services.py
index ad4e549..20c8a44 100644
--- a/tempest/api/identity/admin/v3/test_services.py
+++ b/tempest/api/identity/admin/v3/test_services.py
@@ -77,17 +77,26 @@
     def test_list_services(self):
         # Create, List, Verify and Delete Services
         service_ids = list()
+        service_types = list()
         for _ in range(3):
-            name = data_utils.rand_name('service')
-            serv_type = data_utils.rand_name('type')
+            name = data_utils.rand_name(self.__class__.__name__ + '-Service')
+            serv_type = data_utils.rand_name(self.__class__.__name__ + '-Type')
             create_service = self.services_client.create_service(
                 type=serv_type, name=name)['service']
             self.addCleanup(self.services_client.delete_service,
                             create_service['id'])
             service_ids.append(create_service['id'])
+            service_types.append(serv_type)
 
         # List and Verify Services
         services = self.services_client.list_services()['services']
         fetched_ids = [service['id'] for service in services]
         found = [s for s in fetched_ids if s in service_ids]
         self.assertEqual(len(found), len(service_ids))
+
+        # Check that filtering by service type works.
+        for serv_type in service_types:
+            fetched_services = self.services_client.list_services(
+                type=serv_type)['services']
+            self.assertEqual(1, len(fetched_services))
+            self.assertEqual(serv_type, fetched_services[0]['type'])
diff --git a/tempest/api/identity/base.py b/tempest/api/identity/base.py
index 3bc6ce1..4495cbf 100644
--- a/tempest/api/identity/base.py
+++ b/tempest/api/identity/base.py
@@ -223,6 +223,7 @@
         cls.projects_client = cls.os_admin.projects_client
         cls.role_assignments = cls.os_admin.role_assignments_client
         cls.oauth_consumers_client = cls.os_admin.oauth_consumers_client
+        cls.oauth_token_client = cls.os_admin.oauth_token_client
         cls.domain_config_client = cls.os_admin.domain_config_client
         cls.endpoint_filter_client = cls.os_admin.endpoint_filter_client
         cls.endpoint_groups_client = cls.os_admin.endpoint_groups_client
diff --git a/tempest/api/network/test_tags.py b/tempest/api/network/test_tags.py
index 1f3a7c4..567a462 100644
--- a/tempest/api/network/test_tags.py
+++ b/tempest/api/network/test_tags.py
@@ -14,11 +14,14 @@
 # under the License.
 
 from tempest.api.network import base
+from tempest import config
 from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
 from tempest.lib import exceptions as lib_exc
 from tempest import test
 
+CONF = config.CONF
+
 
 class TagsTest(base.BaseNetworkTest):
     """Tests the following operations in the tags API:
@@ -88,3 +91,112 @@
             self.assertRaises(lib_exc.NotFound,
                               self.tags_client.check_tag_existence, 'networks',
                               self.network['id'], replace_tags[i])
+
+
+class TagsExtTest(base.BaseNetworkTest):
+    """Tests the following operations in the tags API:
+
+        Update all tags.
+        Delete all tags.
+        Check tag existence.
+        Create a tag.
+        List tags.
+        Remove a tag.
+
+    v2.0 of the Neutron API is assumed. The tag-ext extension allows users to
+    set tags on the following resources: subnets, ports, routers and
+    subnetpools.
+    """
+
+    # NOTE(felipemonteiro): The supported resource names are plural. Use
+    # the singular case for the corresponding class resource object.
+    SUPPORTED_RESOURCES = ['subnets', 'ports', 'routers', 'subnetpools']
+
+    @classmethod
+    def skip_checks(cls):
+        super(TagsExtTest, cls).skip_checks()
+        if not test.is_extension_enabled('tag-ext', 'network'):
+            msg = "tag-ext extension not enabled."
+            raise cls.skipException(msg)
+
+    @classmethod
+    def resource_setup(cls):
+        super(TagsExtTest, cls).resource_setup()
+        cls.network = cls.create_network()
+        cls.subnet = cls.create_subnet(cls.network)
+        cls.port = cls.create_port(cls.network)
+        cls.router = cls.create_router()
+
+        subnetpool_name = data_utils.rand_name(cls.__name__ + '-Subnetpool')
+        prefix = CONF.network.default_network
+        cls.subnetpool = cls.subnetpools_client.create_subnetpool(
+            name=subnetpool_name, prefixes=prefix)['subnetpool']
+
+    @classmethod
+    def resource_cleanup(cls):
+        cls.subnetpools_client.delete_subnetpool(cls.subnetpool['id'])
+        super(TagsExtTest, cls).resource_cleanup()
+
+    def _create_tags_for_each_resource(self):
+        # Create a tag for each resource in `SUPPORTED_RESOURCES` and return
+        # the list of tag names.
+        tag_names = []
+
+        for resource in self.SUPPORTED_RESOURCES:
+            tag_name = data_utils.rand_name(self.__class__.__name__ + '-Tag')
+            tag_names.append(tag_name)
+            resource_object = getattr(self, resource[:-1])
+
+            self.tags_client.create_tag(resource, resource_object['id'],
+                                        tag_name)
+            self.addCleanup(self.tags_client.delete_all_tags, resource,
+                            resource_object['id'])
+
+        return tag_names
+
+    @decorators.idempotent_id('c6231efa-9a89-4adf-b050-2a3156b8a1d9')
+    def test_create_check_list_and_delete_tags(self):
+        tag_names = self._create_tags_for_each_resource()
+
+        for i, resource in enumerate(self.SUPPORTED_RESOURCES):
+            # Ensure that a tag was created for each resource.
+            resource_object = getattr(self, resource[:-1])
+            retrieved_tags = self.tags_client.list_tags(
+                resource, resource_object['id'])['tags']
+            self.assertEqual(1, len(retrieved_tags))
+            self.assertEqual(tag_names[i], retrieved_tags[0])
+
+            # Check that the expected tag exists for each resource.
+            self.tags_client.check_tag_existence(
+                resource, resource_object['id'], tag_names[i])
+
+            # Delete the tag and ensure it was deleted.
+            self.tags_client.delete_tag(
+                resource, resource_object['id'], tag_names[i])
+            retrieved_tags = self.tags_client.list_tags(
+                resource, resource_object['id'])['tags']
+            self.assertEmpty(retrieved_tags)
+
+    @decorators.idempotent_id('663a90f5-f334-4b44-afe0-c5fc1d408791')
+    def test_update_and_delete_all_tags(self):
+        self._create_tags_for_each_resource()
+
+        for resource in self.SUPPORTED_RESOURCES:
+            # Generate 3 new tag names.
+            replace_tags = [data_utils.rand_name(
+                self.__class__.__name__ + '-Tag') for _ in range(3)]
+
+            # Replace the current tag with the 3 new tags and validate that the
+            # current resource has the 3 new tags.
+            resource_object = getattr(self, resource[:-1])
+            updated_tags = self.tags_client.update_all_tags(
+                resource, resource_object['id'], replace_tags)['tags']
+            self.assertEqual(3, len(updated_tags))
+            self.assertEqual(set(replace_tags), set(updated_tags))
+
+            # Delete all the tags and check that they have been removed.
+            self.tags_client.delete_all_tags(resource, resource_object['id'])
+            for i in range(3):
+                self.assertRaises(
+                    lib_exc.NotFound, self.tags_client.check_tag_existence,
+                    resource, resource_object['id'], replace_tags[i])
diff --git a/tempest/api/volume/admin/test_groups.py b/tempest/api/volume/admin/test_groups.py
new file mode 100644
index 0000000..8609bdb
--- /dev/null
+++ b/tempest/api/volume/admin/test_groups.py
@@ -0,0 +1,109 @@
+# Copyright (C) 2017 Dell Inc. or its subsidiaries.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.api.volume import base
+from tempest.common import waiters
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+
+class GroupsTest(base.BaseVolumeAdminTest):
+    _api_version = 3
+    min_microversion = '3.14'
+    max_microversion = 'latest'
+
+    def _delete_group(self, grp_id, delete_volumes=True):
+        self.admin_groups_client.delete_group(grp_id, delete_volumes)
+        vols = self.admin_volume_client.list_volumes(detail=True)['volumes']
+        for vol in vols:
+            if vol['group_id'] == grp_id:
+                self.admin_volume_client.wait_for_resource_deletion(vol['id'])
+        self.admin_groups_client.wait_for_resource_deletion(grp_id)
+
+    @decorators.idempotent_id('4b111d28-b73d-4908-9bd2-03dc2992e4d4')
+    def test_group_create_show_list_delete(self):
+        # Create volume type
+        volume_type = self.create_volume_type()
+
+        # Create group type
+        group_type = self.create_group_type()
+
+        # Create group
+        grp1_name = data_utils.rand_name('Group1')
+        grp1 = self.admin_groups_client.create_group(
+            group_type=group_type['id'],
+            volume_types=[volume_type['id']],
+            name=grp1_name)['group']
+        waiters.wait_for_volume_resource_status(
+            self.admin_groups_client, grp1['id'], 'available')
+        grp1_id = grp1['id']
+
+        grp2_name = data_utils.rand_name('Group2')
+        grp2 = self.admin_groups_client.create_group(
+            group_type=group_type['id'],
+            volume_types=[volume_type['id']],
+            name=grp2_name)['group']
+        waiters.wait_for_volume_resource_status(
+            self.admin_groups_client, grp2['id'], 'available')
+        grp2_id = grp2['id']
+
+        # Create volume
+        vol1_name = data_utils.rand_name("volume")
+        params = {'name': vol1_name,
+                  'volume_type': volume_type['id'],
+                  'group_id': grp1['id'],
+                  'size': CONF.volume.volume_size}
+        vol1 = self.admin_volume_client.create_volume(**params)['volume']
+        self.assertEqual(grp1['id'], vol1['group_id'])
+        waiters.wait_for_volume_resource_status(
+            self.admin_volume_client, vol1['id'], 'available')
+        vol1_id = vol1['id']
+
+        # Get a given group
+        grp1 = self.admin_groups_client.show_group(grp1['id'])['group']
+        self.assertEqual(grp1_name, grp1['name'])
+        self.assertEqual(grp1_id, grp1['id'])
+
+        grp2 = self.admin_groups_client.show_group(grp2['id'])['group']
+        self.assertEqual(grp2_name, grp2['name'])
+        self.assertEqual(grp2_id, grp2['id'])
+
+        # Get all groups with detail
+        grps = self.admin_groups_client.list_groups(
+            detail=True)['groups']
+        filtered_grps = [g for g in grps if g['id'] in [grp1_id, grp2_id]]
+        self.assertEqual(2, len(filtered_grps))
+        for grp in filtered_grps:
+            self.assertEqual([volume_type['id']], grp['volume_types'])
+            self.assertEqual(group_type['id'], grp['group_type'])
+
+        vols = self.admin_volume_client.list_volumes(
+            detail=True)['volumes']
+        filtered_vols = [v for v in vols if v['id'] in [vol1_id]]
+        self.assertEqual(1, len(filtered_vols))
+        for vol in filtered_vols:
+            self.assertEqual(grp1_id, vol['group_id'])
+
+        # Delete group
+        # grp1 has a volume so delete_volumes flag is set to True by default
+        self._delete_group(grp1_id)
+        # grp2 is empty so delete_volumes flag can be set to False
+        self._delete_group(grp2_id, delete_volumes=False)
+        grps = self.admin_groups_client.list_groups(
+            detail=True)['groups']
+        self.assertEmpty(grps)
diff --git a/tempest/api/volume/admin/test_snapshot_manage.py b/tempest/api/volume/admin/test_snapshot_manage.py
index 6c09042..9ff7160 100644
--- a/tempest/api/volume/admin/test_snapshot_manage.py
+++ b/tempest/api/volume/admin/test_snapshot_manage.py
@@ -18,6 +18,7 @@
 from tempest import config
 from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
+from tempest.lib import exceptions
 
 CONF = config.CONF
 
@@ -38,8 +39,9 @@
             raise cls.skipException("Manage snapshot tests are disabled")
 
         if len(CONF.volume.manage_snapshot_ref) != 2:
-            raise cls.skipException("Manage snapshot ref is not correctly "
-                                    "configured")
+            msg = ("Manage snapshot ref is not correctly configured, "
+                   "it should be a list of two elements")
+            raise exceptions.InvalidConfiguration(msg)
 
     @decorators.idempotent_id('0132f42d-0147-4b45-8501-cc504bbf7810')
     def test_unmanage_manage_snapshot(self):
diff --git a/tempest/api/volume/admin/test_volume_manage.py b/tempest/api/volume/admin/test_volume_manage.py
index a039085..4b352e0 100644
--- a/tempest/api/volume/admin/test_volume_manage.py
+++ b/tempest/api/volume/admin/test_volume_manage.py
@@ -18,6 +18,7 @@
 from tempest import config
 from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
+from tempest.lib import exceptions
 
 CONF = config.CONF
 
@@ -32,8 +33,9 @@
             raise cls.skipException("Manage volume tests are disabled")
 
         if len(CONF.volume.manage_volume_ref) != 2:
-            raise cls.skipException("Manage volume ref is not correctly "
-                                    "configured")
+            msg = ("Manage volume ref is not correctly configured, "
+                   "it should be a list of two elements")
+            raise exceptions.InvalidConfiguration(msg)
 
     @decorators.idempotent_id('70076c71-0ce1-4208-a8ff-36a66e65cc1e')
     def test_unmanage_manage_volume(self):
diff --git a/tempest/api/volume/admin/test_volume_quotas.py b/tempest/api/volume/admin/test_volume_quotas.py
index ae4b579..f358d7f 100644
--- a/tempest/api/volume/admin/test_volume_quotas.py
+++ b/tempest/api/volume/admin/test_volume_quotas.py
@@ -155,7 +155,7 @@
 
         # Accepts a volume transfer
         self.alt_transfer_client.accept_volume_transfer(
-            transfer_id, auth_key=auth_key)['transfer']
+            transfer_id, auth_key=auth_key)
 
         # Verify volume transferred is available
         waiters.wait_for_volume_resource_status(
diff --git a/tempest/api/volume/api_microversion_fixture.py b/tempest/api/volume/api_microversion_fixture.py
index 64bc537..7bbe674 100644
--- a/tempest/api/volume/api_microversion_fixture.py
+++ b/tempest/api/volume/api_microversion_fixture.py
@@ -13,7 +13,7 @@
 
 import fixtures
 
-from tempest.lib.services.volume.v3 import base_client
+from tempest.lib.services.volume import base_client
 
 
 class APIMicroversionFixture(fixtures.Fixture):
diff --git a/tempest/api/volume/base.py b/tempest/api/volume/base.py
index 8d66156..394c453 100644
--- a/tempest/api/volume/base.py
+++ b/tempest/api/volume/base.py
@@ -71,6 +71,8 @@
 
         cls.snapshots_client = cls.os_primary.snapshots_v2_client
         cls.volumes_client = cls.os_primary.volumes_v2_client
+        if cls._api_version == 3:
+            cls.volumes_client = cls.os_primary.volumes_v3_client
         cls.backups_client = cls.os_primary.backups_v2_client
         cls.volumes_extension_client =\
             cls.os_primary.volumes_v2_extension_client
@@ -79,9 +81,7 @@
         cls.volume_limits_client = cls.os_primary.volume_v2_limits_client
         cls.messages_client = cls.os_primary.volume_v3_messages_client
         cls.versions_client = cls.os_primary.volume_v3_versions_client
-
-        if cls._api_version == 3:
-            cls.volumes_client = cls.os_primary.volumes_v3_client
+        cls.groups_client = cls.os_primary.groups_v3_client
 
     def setUp(self):
         super(BaseVolumeTest, self).setUp()
@@ -253,6 +253,8 @@
         cls.admin_volume_types_client = cls.os_admin.volume_types_v2_client
         cls.admin_volume_manage_client = cls.os_admin.volume_manage_v2_client
         cls.admin_volume_client = cls.os_admin.volumes_v2_client
+        if cls._api_version == 3:
+            cls.admin_volume_client = cls.os_admin.volumes_v3_client
         cls.admin_hosts_client = cls.os_admin.volume_hosts_v2_client
         cls.admin_snapshot_manage_client = \
             cls.os_admin.snapshot_manage_v2_client
@@ -269,9 +271,8 @@
         cls.admin_scheduler_stats_client = \
             cls.os_admin.volume_scheduler_stats_v2_client
         cls.admin_messages_client = cls.os_admin.volume_v3_messages_client
-
-        if cls._api_version == 3:
-            cls.admin_volume_client = cls.os_admin.volumes_v3_client
+        cls.admin_groups_client = cls.os_admin.groups_v3_client
+        cls.admin_group_types_client = cls.os_admin.group_types_v3_client
 
     @classmethod
     def resource_setup(cls):
@@ -305,6 +306,16 @@
         cls.volume_types.append(volume_type['id'])
         return volume_type
 
+    def create_group_type(self, name=None, **kwargs):
+        """Create a test group-type"""
+        name = name or data_utils.rand_name(
+            self.__class__.__name__ + '-group-type')
+        group_type = self.admin_group_types_client.create_group_type(
+            name=name, **kwargs)['group_type']
+        self.addCleanup(self.admin_group_types_client.delete_group_type,
+                        group_type['id'])
+        return group_type
+
     @classmethod
     def clear_qos_specs(cls):
         for qos_id in cls.qos_specs:
diff --git a/tempest/api/volume/test_snapshot_metadata.py b/tempest/api/volume/test_snapshot_metadata.py
index 164ed37..e54cd65 100644
--- a/tempest/api/volume/test_snapshot_metadata.py
+++ b/tempest/api/volume/test_snapshot_metadata.py
@@ -55,6 +55,7 @@
         # Create metadata
         body = self.snapshots_client.create_snapshot_metadata(
             self.snapshot['id'], metadata)['metadata']
+        self.assertThat(body.items(), matchers.ContainsAll(metadata.items()))
 
         # Get the metadata of the snapshot
         body = self.snapshots_client.show_snapshot_metadata(
@@ -65,6 +66,7 @@
         # Update metadata
         body = self.snapshots_client.update_snapshot_metadata(
             self.snapshot['id'], metadata=update)['metadata']
+        self.assertEqual(update, body)
         body = self.snapshots_client.show_snapshot_metadata(
             self.snapshot['id'])['metadata']
         self.assertEqual(update, body, 'Update snapshot metadata failed')
@@ -89,8 +91,8 @@
                   "key2": "value2",
                   "key3": "value3_update"}
         # Create metadata for the snapshot
-        body = self.snapshots_client.create_snapshot_metadata(
-            self.snapshot['id'], metadata)['metadata']
+        self.snapshots_client.create_snapshot_metadata(
+            self.snapshot['id'], metadata)
         # Get the metadata of the snapshot
         body = self.snapshots_client.show_snapshot_metadata(
             self.snapshot['id'])['metadata']
@@ -98,6 +100,7 @@
         # Update metadata item
         body = self.snapshots_client.update_snapshot_metadata_item(
             self.snapshot['id'], "key3", meta=update_item)['meta']
+        self.assertEqual(update_item, body)
         # Get the metadata of the snapshot
         body = self.snapshots_client.show_snapshot_metadata(
             self.snapshot['id'])['metadata']
diff --git a/tempest/api/volume/test_volume_metadata.py b/tempest/api/volume/test_volume_metadata.py
index b4d22fe..5e9a956 100644
--- a/tempest/api/volume/test_volume_metadata.py
+++ b/tempest/api/volume/test_volume_metadata.py
@@ -45,6 +45,7 @@
 
         body = self.volumes_client.create_volume_metadata(self.volume['id'],
                                                           metadata)['metadata']
+        self.assertThat(body.items(), matchers.ContainsAll(metadata.items()))
         # Get the metadata of the volume
         body = self.volumes_client.show_volume_metadata(
             self.volume['id'])['metadata']
@@ -54,6 +55,7 @@
         # Update metadata
         body = self.volumes_client.update_volume_metadata(
             self.volume['id'], update)['metadata']
+        self.assertEqual(update, body)
         body = self.volumes_client.show_volume_metadata(
             self.volume['id'])['metadata']
         self.assertEqual(update, body, 'Update metadata failed')
@@ -85,6 +87,7 @@
         # Update metadata item
         body = self.volumes_client.update_volume_metadata_item(
             self.volume['id'], "key3", update_item)['meta']
+        self.assertEqual(update_item, body)
         # Get the metadata of the volume
         body = self.volumes_client.show_volume_metadata(
             self.volume['id'])['metadata']
diff --git a/tempest/api/volume/test_volume_transfers.py b/tempest/api/volume/test_volume_transfers.py
index 2c13a3c..4108da5 100644
--- a/tempest/api/volume/test_volume_transfers.py
+++ b/tempest/api/volume/test_volume_transfers.py
@@ -59,6 +59,8 @@
         # Accept a volume transfer by alt_tenant
         body = self.alt_client.accept_volume_transfer(
             transfer_id, auth_key=auth_key)['transfer']
+        for key in ['id', 'name', 'links', 'volume_id']:
+            self.assertIn(key, body)
         waiters.wait_for_volume_resource_status(self.alt_volumes_client,
                                                 volume['id'], 'available')
         accepted_volume = self.alt_volumes_client.show_volume(
diff --git a/tempest/api/volume/test_volumes_actions.py b/tempest/api/volume/test_volumes_actions.py
index 8541c6d..c4d10c3 100644
--- a/tempest/api/volume/test_volumes_actions.py
+++ b/tempest/api/volume/test_volumes_actions.py
@@ -115,12 +115,12 @@
     @decorators.idempotent_id('92c4ef64-51b2-40c0-9f7e-4749fbaaba33')
     def test_reserve_unreserve_volume(self):
         # Mark volume as reserved.
-        body = self.volumes_client.reserve_volume(self.volume['id'])
+        self.volumes_client.reserve_volume(self.volume['id'])
         # To get the volume info
         body = self.volumes_client.show_volume(self.volume['id'])['volume']
         self.assertIn('attaching', body['status'])
         # Unmark volume as reserved.
-        body = self.volumes_client.unreserve_volume(self.volume['id'])
+        self.volumes_client.unreserve_volume(self.volume['id'])
         # To get the volume info
         body = self.volumes_client.show_volume(self.volume['id'])['volume']
         self.assertIn('available', body['status'])
diff --git a/tempest/clients.py b/tempest/clients.py
index 8ad6e92..c3357bb 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -200,6 +200,8 @@
             **params_v3)
         self.oauth_consumers_client = self.identity_v3.OAUTHConsumerClient(
             **params_v3)
+        self.oauth_token_client = self.identity_v3.OAUTHTokenClient(
+            **params_v3)
         self.domain_config_client = self.identity_v3.DomainConfigurationClient(
             **params_v3)
         self.endpoint_filter_client = \
@@ -255,6 +257,8 @@
             self.volume_v2.QuotaClassesClient()
         self.volumes_extension_client = self.volume_v1.ExtensionsClient()
         self.volumes_v2_extension_client = self.volume_v2.ExtensionsClient()
+        self.groups_v3_client = self.volume_v3.GroupsClient()
+        self.group_types_v3_client = self.volume_v3.GroupTypesClient()
         self.volume_availability_zone_client = \
             self.volume_v1.AvailabilityZoneClient()
         self.volume_v2_availability_zone_client = \
diff --git a/tempest/cmd/verify_tempest_config.py b/tempest/cmd/verify_tempest_config.py
index 6b81e0d..0972a3c 100644
--- a/tempest/cmd/verify_tempest_config.py
+++ b/tempest/cmd/verify_tempest_config.py
@@ -97,8 +97,9 @@
         'keystone': os.identity_client,
         'cinder': os.volumes_client,
     }
-    if service != 'keystone':
-        # Since keystone may be listening on a path, do not remove the path.
+    if service != 'keystone' and service != 'cinder':
+        # Since keystone and cinder may be listening on a path,
+        # do not remove the path.
         client_dict[service].skip_path()
     endpoint = _get_unversioned_endpoint(client_dict[service].base_url)
 
diff --git a/tempest/cmd/workspace.py b/tempest/cmd/workspace.py
index 96d2300..8166b4f 100644
--- a/tempest/cmd/workspace.py
+++ b/tempest/cmd/workspace.py
@@ -40,6 +40,8 @@
 ------
 Deletes the entry for a given tempest workspace --name
 
+--rmdir Deletes the given tempest workspace directory
+
 General Options
 ===============
 
@@ -49,6 +51,7 @@
 """
 
 import os
+import shutil
 import sys
 
 from cliff import command
@@ -102,11 +105,16 @@
             sys.exit(1)
 
     @lockutils.synchronized('workspaces', external=True)
-    def remove_workspace(self, name):
+    def remove_workspace_entry(self, name):
         self._populate()
         self._name_exists(name)
-        self.workspaces.pop(name)
+        workspace_path = self.workspaces.pop(name)
         self._write_file()
+        return workspace_path
+
+    @lockutils.synchronized('workspaces', external=True)
+    def remove_workspace_directory(self, workspace_path):
+        shutil.rmtree(workspace_path)
 
     @lockutils.synchronized('workspaces', external=True)
     def list_workspaces(self):
@@ -226,12 +234,16 @@
         parser = super(TempestWorkspaceRemove, self).get_parser(prog_name)
         add_global_arguments(parser)
         parser.add_argument('--name', required=True)
+        parser.add_argument('--rmdir', action='store_true',
+                            help='Deletes the given workspace directory')
 
         return parser
 
     def take_action(self, parsed_args):
         self.manager = WorkspaceManager(parsed_args.workspace_path)
-        self.manager.remove_workspace(parsed_args.name)
+        workspace_path = self.manager.remove_workspace_entry(parsed_args.name)
+        if parsed_args.rmdir:
+            self.manager.remove_workspace_directory(workspace_path)
         sys.exit(0)
 
 
diff --git a/tempest/common/compute.py b/tempest/common/compute.py
index 9110c4a..9f467fe 100644
--- a/tempest/common/compute.py
+++ b/tempest/common/compute.py
@@ -204,6 +204,19 @@
                         except Exception:
                             LOG.exception('Deleting server %s failed',
                                           server['id'])
+                    for server in servers:
+                        # NOTE(artom) If the servers were booted with volumes
+                        # and with delete_on_termination=False we need to wait
+                        # for the servers to go away before proceeding with
+                        # cleanup, otherwise we'll attempt to delete the
+                        # volumes while they're still attached to servers that
+                        # are in the process of being deleted.
+                        try:
+                            waiters.wait_for_server_termination(
+                                clients.servers_client, server['id'])
+                        except Exception:
+                            LOG.exception('Server %s failed to delete in time',
+                                          server['id'])
 
     return body, servers
 
@@ -252,16 +265,34 @@
     def __init__(self, client_socket, url):
         """Contructor for the WebSocket wrapper to the socket."""
         self._socket = client_socket
+        # cached stream for early frames.
+        self.cached_stream = b''
         # Upgrade the HTTP connection to a WebSocket
         self._upgrade(url)
 
+    def _recv(self, recv_size):
+        """Wrapper to receive data from the cached stream or socket."""
+        if recv_size <= 0:
+            return None
+
+        data_from_cached = b''
+        data_from_socket = b''
+        if len(self.cached_stream) > 0:
+            read_from_cached = min(len(self.cached_stream), recv_size)
+            data_from_cached += self.cached_stream[:read_from_cached]
+            self.cached_stream = self.cached_stream[read_from_cached:]
+            recv_size -= read_from_cached
+        if recv_size > 0:
+            data_from_socket = self._socket.recv(recv_size)
+        return data_from_cached + data_from_socket
+
     def receive_frame(self):
         """Wrapper for receiving data to parse the WebSocket frame format"""
         # We need to loop until we either get some bytes back in the frame
         # or no data was received (meaning the socket was closed).  This is
         # done to handle the case where we get back some empty frames
         while True:
-            header = self._socket.recv(2)
+            header = self._recv(2)
             # If we didn't receive any data, just return None
             if not header:
                 return None
@@ -270,7 +301,7 @@
             # that only the 2nd byte contains the length, and since the
             # server doesn't do masking, we can just read the data length
             if ord_func(header[1]) & 127 > 0:
-                return self._socket.recv(ord_func(header[1]) & 127)
+                return self._recv(ord_func(header[1]) & 127)
 
     def send_frame(self, data):
         """Wrapper for sending data to add in the WebSocket frame format."""
@@ -318,6 +349,15 @@
         self._socket.sendall(reqdata.encode('utf8'))
         self.response = data = self._socket.recv(4096)
         # Loop through & concatenate all of the data in the response body
-        while data and self.response.find(b'\r\n\r\n') < 0:
+        end_loc = self.response.find(b'\r\n\r\n')
+        while data and end_loc < 0:
             data = self._socket.recv(4096)
             self.response += data
+            end_loc = self.response.find(b'\r\n\r\n')
+
+        if len(self.response) > end_loc + 4:
+            # In case some frames (e.g. the first RFP negotiation) have
+            # arrived, cache it for next reading.
+            self.cached_stream = self.response[end_loc + 4:]
+            # ensure response ends with '\r\n\r\n'.
+            self.response = self.response[:end_loc + 4]
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index 9c83c99..93e6fbf 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -186,8 +186,9 @@
     resources. The function extracts the name of the desired resource from
     the client class name of the resource.
     """
-    resource_name = re.findall(r'(Volume|Snapshot|Backup)',
-                               client.__class__.__name__)[0].lower()
+    resource_name = re.findall(
+        r'(Volume|Snapshot|Backup|Group)',
+        client.__class__.__name__)[0].lower()
     show_resource = getattr(client, 'show_' + resource_name)
     resource_status = show_resource(resource_id)[resource_name]['status']
     start = int(time.time())
diff --git a/tempest/config.py b/tempest/config.py
index fdf6f0a..af9eefc 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -832,7 +832,7 @@
                 default=True,
                 help="Is the v2 volume API enabled"),
     cfg.BoolOpt('api_v3',
-                default=False,
+                default=True,
                 help="Is the v3 volume API enabled")
 ]
 
diff --git a/tempest/lib/api_schema/response/compute/v2_1/servers.py b/tempest/lib/api_schema/response/compute/v2_1/servers.py
index 33a7757..7360396 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/servers.py
@@ -582,3 +582,10 @@
         'required': ['adminPass']
     }
 }
+
+show_server_diagnostics = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object'
+    }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_47/__init__.py b/tempest/lib/api_schema/response/compute/v2_47/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_47/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_47/servers.py b/tempest/lib/api_schema/response/compute/v2_47/servers.py
new file mode 100644
index 0000000..37a084f
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_47/servers.py
@@ -0,0 +1,39 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import copy
+
+from tempest.lib.api_schema.response.compute.v2_26 import servers as servers226
+
+flavor = {
+    'type': 'object',
+    'properties': {
+        'original_name': {'type': 'string'},
+        'disk': {'type': 'integer'},
+        'ephemeral': {'type': 'integer'},
+        'ram': {'type': 'integer'},
+        'swap': {'type': 'integer'},
+        'vcpus': {'type': 'integer'},
+        'extra_specs': {
+            'type': 'object',
+            'patternProperties': {
+                '^[a-zA-Z0-9_\-\. :]+$': {'type': 'string'}
+            }
+        }
+    },
+    'additionalProperties': False,
+    'required': ['original_name', 'disk', 'ephemeral', 'ram', 'swap', 'vcpus']
+}
+
+get_server = copy.deepcopy(servers226.get_server)
+get_server['response_body']['properties']['server'][
+    'properties'].update({'flavor': flavor})
diff --git a/tempest/lib/api_schema/response/compute/v2_48/__init__.py b/tempest/lib/api_schema/response/compute/v2_48/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_48/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_48/servers.py b/tempest/lib/api_schema/response/compute/v2_48/servers.py
new file mode 100644
index 0000000..5904758
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_48/servers.py
@@ -0,0 +1,115 @@
+# Copyright 2017 Mirantis Inc.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import copy
+
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+from tempest.lib.api_schema.response.compute.v2_47 import servers as servers247
+
+
+show_server_diagnostics = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'state': {
+                'type': 'string', 'enum': [
+                    'pending', 'running', 'paused', 'shutdown', 'crashed',
+                    'suspended']
+            },
+            'driver': {
+                'type': 'string', 'enum': [
+                    'libvirt', 'xenapi', 'vmwareapi', 'ironic', 'hyperv']
+            },
+            'hypervisor': {'type': ['string', 'null']},
+            'hypervisor_os': {'type': ['string', 'null']},
+            'uptime': {'type': ['integer', 'null']},
+            'config_drive': {'type': 'boolean'},
+            'num_cpus': {'type': 'integer'},
+            'num_nics': {'type': 'integer'},
+            'num_disks': {'type': 'integer'},
+            'memory_details': {
+                'type': 'object',
+                'properties': {
+                    'maximum': {'type': ['integer', 'null']},
+                    'used': {'type': ['integer', 'null']}
+                },
+                'additionalProperties': False,
+                'required': ['maximum', 'used']
+            },
+            'cpu_details': {
+                'type': 'array',
+                'items': {
+                    'type': 'object',
+                    'properties': {
+                        'id': {'type': ['integer', 'null']},
+                        'time': {'type': ['integer', 'null']},
+                        'utilisation': {'type': ['integer', 'null']}
+                    },
+                    'additionalProperties': False,
+                    'required': ['id', 'time', 'utilisation']
+                }
+            },
+            'nic_details': {
+                'type': 'array',
+                'items': {
+                    'type': 'object',
+                    'properties': {
+                        'mac_address': {'oneOf': [parameter_types.mac_address,
+                                                  {'type': 'null'}]},
+                        'rx_octets': {'type': ['integer', 'null']},
+                        'rx_errors': {'type': ['integer', 'null']},
+                        'rx_drop': {'type': ['integer', 'null']},
+                        'rx_packets': {'type': ['integer', 'null']},
+                        'rx_rate': {'type': ['integer', 'null']},
+                        'tx_octets': {'type': ['integer', 'null']},
+                        'tx_errors': {'type': ['integer', 'null']},
+                        'tx_drop': {'type': ['integer', 'null']},
+                        'tx_packets': {'type': ['integer', 'null']},
+                        'tx_rate': {'type': ['integer', 'null']}
+                    },
+                    'additionalProperties': False,
+                    'required': ['mac_address', 'rx_octets', 'rx_errors',
+                                 'rx_drop',
+                                 'rx_packets', 'rx_rate', 'tx_octets',
+                                 'tx_errors',
+                                 'tx_drop', 'tx_packets', 'tx_rate']
+                }
+            },
+            'disk_details': {
+                'type': 'array',
+                'items': {
+                    'type': 'object',
+                    'properties': {
+                        'read_bytes': {'type': ['integer', 'null']},
+                        'read_requests': {'type': ['integer', 'null']},
+                        'write_bytes': {'type': ['integer', 'null']},
+                        'write_requests': {'type': ['integer', 'null']},
+                        'errors_count': {'type': ['integer', 'null']}
+                    },
+                    'additionalProperties': False,
+                    'required': ['read_bytes', 'read_requests', 'write_bytes',
+                                 'write_requests', 'errors_count']
+                }
+            }
+        },
+        'additionalProperties': False,
+        'required': [
+            'state', 'driver', 'hypervisor', 'hypervisor_os', 'uptime',
+            'config_drive', 'num_cpus', 'num_nics', 'num_disks',
+            'memory_details', 'cpu_details', 'nic_details', 'disk_details'],
+    }
+}
+
+get_server = copy.deepcopy(servers247.get_server)
diff --git a/tempest/lib/services/compute/baremetal_nodes_client.py b/tempest/lib/services/compute/baremetal_nodes_client.py
index 06dc369..e44c195 100644
--- a/tempest/lib/services/compute/baremetal_nodes_client.py
+++ b/tempest/lib/services/compute/baremetal_nodes_client.py
@@ -25,7 +25,12 @@
     """Tests Baremetal API"""
 
     def list_baremetal_nodes(self, **params):
-        """List all baremetal nodes."""
+        """List all baremetal nodes.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/compute/#list-bare-metal-nodes
+        """
         url = 'os-baremetal-nodes'
         if params:
             url += '?%s' % urllib.urlencode(params)
@@ -35,7 +40,11 @@
         return rest_client.ResponseBody(resp, body)
 
     def show_baremetal_node(self, baremetal_node_id):
-        """Return the details of a single baremetal node."""
+        """Show the details of a single baremetal node.
+
+        For more information, please refer to the official API reference:
+        https://developer.openstack.org/api-ref/compute/#show-bare-metal-node-details
+        """
         url = 'os-baremetal-nodes/%s' % baremetal_node_id
         resp, body = self.get(url)
         body = json.loads(body)
diff --git a/tempest/lib/services/compute/servers_client.py b/tempest/lib/services/compute/servers_client.py
index ff65b25..598d5a6 100644
--- a/tempest/lib/services/compute/servers_client.py
+++ b/tempest/lib/services/compute/servers_client.py
@@ -27,6 +27,8 @@
 from tempest.lib.api_schema.response.compute.v2_19 import servers as schemav219
 from tempest.lib.api_schema.response.compute.v2_26 import servers as schemav226
 from tempest.lib.api_schema.response.compute.v2_3 import servers as schemav23
+from tempest.lib.api_schema.response.compute.v2_47 import servers as schemav247
+from tempest.lib.api_schema.response.compute.v2_48 import servers as schemav248
 from tempest.lib.api_schema.response.compute.v2_6 import servers as schemav26
 from tempest.lib.api_schema.response.compute.v2_9 import servers as schemav29
 from tempest.lib.common import rest_client
@@ -43,7 +45,9 @@
         {'min': '2.9', 'max': '2.15', 'schema': schemav29},
         {'min': '2.16', 'max': '2.18', 'schema': schemav216},
         {'min': '2.19', 'max': '2.25', 'schema': schemav219},
-        {'min': '2.26', 'max': None, 'schema': schemav226}]
+        {'min': '2.26', 'max': '2.46', 'schema': schemav226},
+        {'min': '2.47', 'max': '2.47', 'schema': schemav247},
+        {'min': '2.48', 'max': None, 'schema': schemav248}]
 
     def __init__(self, auth_provider, service, region,
                  enable_instance_password=True, **kwargs):
@@ -656,7 +660,10 @@
     def show_server_diagnostics(self, server_id):
         """Get the usage data for a server."""
         resp, body = self.get("servers/%s/diagnostics" % server_id)
-        return rest_client.ResponseBody(resp, json.loads(body))
+        body = json.loads(body)
+        schema = self.get_schema(self.schema_versions_info)
+        self.validate_response(schema.show_server_diagnostics, resp, body)
+        return rest_client.ResponseBody(resp, body)
 
     def list_instance_actions(self, server_id):
         """List the provided server action."""
diff --git a/tempest/lib/services/identity/v3/__init__.py b/tempest/lib/services/identity/v3/__init__.py
index ce607ff..e271a58 100644
--- a/tempest/lib/services/identity/v3/__init__.py
+++ b/tempest/lib/services/identity/v3/__init__.py
@@ -28,6 +28,8 @@
     InheritedRolesClient
 from tempest.lib.services.identity.v3.oauth_consumers_client import \
     OAUTHConsumerClient
+from tempest.lib.services.identity.v3.oauth_token_client import \
+    OAUTHTokenClient
 from tempest.lib.services.identity.v3.policies_client import PoliciesClient
 from tempest.lib.services.identity.v3.projects_client import ProjectsClient
 from tempest.lib.services.identity.v3.regions_client import RegionsClient
@@ -43,7 +45,7 @@
 __all__ = ['CredentialsClient', 'DomainsClient', 'DomainConfigurationClient',
            'EndPointGroupsClient', 'EndPointsClient', 'EndPointsFilterClient',
            'GroupsClient', 'IdentityClient', 'InheritedRolesClient',
-           'OAUTHConsumerClient', 'PoliciesClient', 'ProjectsClient',
-           'RegionsClient', 'RoleAssignmentsClient', 'RolesClient',
-           'ServicesClient', 'V3TokenClient', 'TrustsClient', 'UsersClient',
-           'VersionsClient']
+           'OAUTHConsumerClient', 'OAUTHTokenClient', 'PoliciesClient',
+           'ProjectsClient', 'RegionsClient', 'RoleAssignmentsClient',
+           'RolesClient', 'ServicesClient', 'V3TokenClient', 'TrustsClient',
+           'UsersClient', 'VersionsClient']
diff --git a/tempest/lib/services/identity/v3/endpoints_client.py b/tempest/lib/services/identity/v3/endpoints_client.py
index 91592de..e24dca7 100644
--- a/tempest/lib/services/identity/v3/endpoints_client.py
+++ b/tempest/lib/services/identity/v3/endpoints_client.py
@@ -18,6 +18,7 @@
 """
 
 from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
 
 from tempest.lib.common import rest_client
 
@@ -25,9 +26,17 @@
 class EndPointsClient(rest_client.RestClient):
     api_version = "v3"
 
-    def list_endpoints(self):
-        """GET endpoints."""
-        resp, body = self.get('endpoints')
+    def list_endpoints(self, **params):
+        """List endpoints.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        http://developer.openstack.org/api-ref/identity/v3/#list-endpoints
+        """
+        url = 'endpoints'
+        if params:
+            url += '?%s' % urllib.urlencode(params)
+        resp, body = self.get(url)
         self.expected_success(200, resp.status)
         body = json.loads(body)
         return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v3/oauth_token_client.py b/tempest/lib/services/identity/v3/oauth_token_client.py
new file mode 100644
index 0000000..b1d298b
--- /dev/null
+++ b/tempest/lib/services/identity/v3/oauth_token_client.py
@@ -0,0 +1,236 @@
+# Copyright 2017 AT&T 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.
+
+import binascii
+import hashlib
+import hmac
+import random
+import time
+
+import six
+from six.moves.urllib import parse as urlparse
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class OAUTHTokenClient(rest_client.RestClient):
+    api_version = "v3"
+
+    def _escape(self, s):
+        """Escape a unicode string in an OAuth-compatible fashion."""
+        safe = b'~'
+        s = s.encode('utf-8') if isinstance(s, six.text_type) else s
+        s = urlparse.quote(s, safe)
+        if isinstance(s, six.binary_type):
+            s = s.decode('utf-8')
+        return s
+
+    def _generate_params_with_signature(self, client_key, uri,
+                                        client_secret=None,
+                                        resource_owner_key=None,
+                                        resource_owner_secret=None,
+                                        callback_uri=None,
+                                        verifier=None,
+                                        http_method='GET'):
+        """Generate OAUTH params along with signature."""
+        timestamp = six.text_type(int(time.time()))
+        nonce = six.text_type(random.getrandbits(64)) + timestamp
+        oauth_params = [
+            ('oauth_nonce', nonce),
+            ('oauth_timestamp', timestamp),
+            ('oauth_version', '1.0'),
+            ('oauth_signature_method', 'HMAC-SHA1'),
+            ('oauth_consumer_key', client_key),
+        ]
+        if resource_owner_key:
+            oauth_params.append(('oauth_token', resource_owner_key))
+        if callback_uri:
+            oauth_params.append(('oauth_callback', callback_uri))
+        if verifier:
+            oauth_params.append(('oauth_verifier', verifier))
+
+        # normalize_params
+        key_values = [(self._escape(k), self._escape(v))
+                      for k, v in oauth_params]
+        key_values.sort()
+        parameter_parts = ['{0}={1}'.format(k, v) for k, v in key_values]
+        normalized_params = '&'.join(parameter_parts)
+
+        # normalize_uri
+        scheme, netloc, path, params, query, fragment = urlparse.urlparse(uri)
+        scheme = scheme.lower()
+        netloc = netloc.lower()
+        normalized_uri = urlparse.urlunparse((scheme, netloc, path,
+                                              params, '', ''))
+
+        # construct base string
+        base_string = self._escape(http_method.upper())
+        base_string += '&'
+        base_string += self._escape(normalized_uri)
+        base_string += '&'
+        base_string += self._escape(normalized_params)
+
+        # sign using hmac-sha1
+        key = self._escape(client_secret or '')
+        key += '&'
+        key += self._escape(resource_owner_secret or '')
+        key_utf8 = key.encode('utf-8')
+        text_utf8 = base_string.encode('utf-8')
+        signature = hmac.new(key_utf8, text_utf8, hashlib.sha1)
+        sig = binascii.b2a_base64(signature.digest())[:-1].decode('utf-8')
+
+        oauth_params.append(('oauth_signature', sig))
+        return oauth_params
+
+    def _generate_oauth_header(self, oauth_params):
+        authorization_header = {}
+        authorization_header_parameters_parts = []
+        for oauth_parameter_name, value in oauth_params:
+            escaped_name = self._escape(oauth_parameter_name)
+            escaped_value = self._escape(value)
+            part = '{0}="{1}"'.format(escaped_name, escaped_value)
+            authorization_header_parameters_parts.append(part)
+
+        authorization_header_parameters = ', '.join(
+            authorization_header_parameters_parts)
+        oauth_string = 'OAuth %s' % authorization_header_parameters
+        authorization_header['Authorization'] = oauth_string
+
+        return authorization_header
+
+    def create_request_token(self, consumer_key, consumer_secret, project_id):
+        """Create request token.
+
+        For more information, please refer to the official API reference:
+        http://developer.openstack.org/api-ref/identity/v3-ext/#create-request-token
+        """
+        endpoint = 'OS-OAUTH1/request_token'
+        headers = {'Requested-Project-Id': project_id}
+        oauth_params = self._generate_params_with_signature(
+            consumer_key,
+            self.base_url + '/' + endpoint,
+            client_secret=consumer_secret,
+            callback_uri='oob',
+            http_method='POST')
+        oauth_header = self._generate_oauth_header(oauth_params)
+        headers.update(oauth_header)
+        resp, body = self.post(endpoint,
+                               body=None,
+                               headers=headers)
+        self.expected_success(201, resp.status)
+        if not isinstance(body, str):
+            body = body.decode('utf-8')
+        body = dict(item.split("=") for item in body.split("&"))
+        return rest_client.ResponseBody(resp, body)
+
+    def authorize_request_token(self, request_token_id, role_ids):
+        """Authorize request token.
+
+        For more information, please refer to the official API reference:
+        http://developer.openstack.org/api-ref/identity/v3-ext/#authorize-request-token
+        """
+        roles = [{'id': role_id} for role_id in role_ids]
+        body = {'roles': roles}
+        post_body = json.dumps(body)
+        resp, body = self.put("OS-OAUTH1/authorize/%s" % request_token_id,
+                              post_body)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def create_access_token(self, consumer_key, consumer_secret, request_key,
+                            request_secret, oauth_verifier):
+        """Create access token.
+
+        For more information, please refer to the official API reference:
+        http://developer.openstack.org/api-ref/identity/v3-ext/#create-access-token
+        """
+        endpoint = 'OS-OAUTH1/access_token'
+        oauth_params = self._generate_params_with_signature(
+            consumer_key,
+            self.base_url + '/' + endpoint,
+            client_secret=consumer_secret,
+            resource_owner_key=request_key,
+            resource_owner_secret=request_secret,
+            verifier=oauth_verifier,
+            http_method='POST')
+        headers = self._generate_oauth_header(oauth_params)
+        resp, body = self.post(endpoint, body=None, headers=headers)
+        self.expected_success(201, resp.status)
+        if not isinstance(body, str):
+            body = body.decode('utf-8')
+        body = dict(item.split("=") for item in body.split("&"))
+        return rest_client.ResponseBody(resp, body)
+
+    def get_access_token(self, user_id, access_token_id):
+        """Get access token.
+
+        For more information, please refer to the official API reference:
+        http://developer.openstack.org/api-ref/identity/v3-ext/#get-access-token
+        """
+        resp, body = self.get("users/%s/OS-OAUTH1/access_tokens/%s"
+                              % (user_id, access_token_id))
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def revoke_access_token(self, user_id, access_token_id):
+        """Revoke access token.
+
+        For more information, please refer to the official API reference:
+        http://developer.openstack.org/api-ref/identity/v3-ext/#revoke-access-token
+        """
+        resp, body = self.delete("users/%s/OS-OAUTH1/access_tokens/%s"
+                                 % (user_id, access_token_id))
+        self.expected_success(204, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def list_access_tokens(self, user_id):
+        """List access tokens.
+
+        For more information, please refer to the official API reference:
+        http://developer.openstack.org/api-ref/identity/v3-ext/#list-access-tokens
+        """
+        resp, body = self.get("users/%s/OS-OAUTH1/access_tokens"
+                              % (user_id))
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def list_access_token_roles(self, user_id, access_token_id):
+        """List roles for an access token.
+
+        For more information, please refer to the official API reference:
+        http://developer.openstack.org/api-ref/identity/v3-ext/#list-roles-for-an-access-token
+        """
+        resp, body = self.get("users/%s/OS-OAUTH1/access_tokens/%s/roles"
+                              % (user_id, access_token_id))
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def get_access_token_role(self, user_id, access_token_id, role_id):
+        """Show role details for an access token.
+
+        For more information, please refer to the official API reference:
+        http://developer.openstack.org/api-ref/identity/v3-ext/#show-role-details-for-an-access-token
+        """
+        resp, body = self.get("users/%s/OS-OAUTH1/access_tokens/%s/roles/%s"
+                              % (user_id, access_token_id, role_id))
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/network/base.py b/tempest/lib/services/network/base.py
index b6f9c91..fe8b244 100644
--- a/tempest/lib/services/network/base.py
+++ b/tempest/lib/services/network/base.py
@@ -54,7 +54,8 @@
         self.expected_success(200, resp.status)
         return rest_client.ResponseBody(resp, body)
 
-    def create_resource(self, uri, post_data, expect_empty_body=False):
+    def create_resource(self, uri, post_data, expect_empty_body=False,
+                        expect_response_code=201):
         req_uri = self.uri_prefix + uri
         req_post_data = json.dumps(post_data)
         resp, body = self.post(req_uri, req_post_data)
@@ -65,10 +66,11 @@
             body = json.loads(body)
         else:
             body = None
-        self.expected_success(201, resp.status)
+        self.expected_success(expect_response_code, resp.status)
         return rest_client.ResponseBody(resp, body)
 
-    def update_resource(self, uri, post_data, expect_empty_body=False):
+    def update_resource(self, uri, post_data, expect_empty_body=False,
+                        expect_response_code=200):
         req_uri = self.uri_prefix + uri
         req_post_data = json.dumps(post_data)
         resp, body = self.put(req_uri, req_post_data)
@@ -79,5 +81,5 @@
             body = json.loads(body)
         else:
             body = None
-        self.expected_success(200, resp.status)
+        self.expected_success(expect_response_code, resp.status)
         return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/network/tags_client.py b/tempest/lib/services/network/tags_client.py
index 20c2c11..5d49a79 100644
--- a/tempest/lib/services/network/tags_client.py
+++ b/tempest/lib/services/network/tags_client.py
@@ -27,14 +27,10 @@
         For more information, please refer to the official API reference:
         http://developer.openstack.org/api-ref/networking/v2/index.html#add-a-tag
         """
-        # NOTE(felipemonteiro): Cannot use ``update_resource`` method because
-        # this API requires self.put but returns 201 instead of 200 expected
-        # by ``update_resource``.
-        uri = '%s/%s/%s/tags/%s' % (
-            self.uri_prefix, resource_type, resource_id, tag)
-        resp, _ = self.put(uri, json.dumps({}))
-        self.expected_success(201, resp.status)
-        return rest_client.ResponseBody(resp)
+        uri = '/%s/%s/tags/%s' % (resource_type, resource_id, tag)
+        return self.update_resource(
+            uri, json.dumps({}), expect_response_code=201,
+            expect_empty_body=True)
 
     def check_tag_existence(self, resource_type, resource_id, tag):
         """Confirm that a given tag is set on the resource.
diff --git a/tempest/lib/services/volume/base_client.py b/tempest/lib/services/volume/base_client.py
new file mode 100644
index 0000000..c7fb21a
--- /dev/null
+++ b/tempest/lib/services/volume/base_client.py
@@ -0,0 +1,45 @@
+# Copyright 2016 Andrew Kerr
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.lib.common import api_version_utils
+from tempest.lib.common import rest_client
+
+VOLUME_MICROVERSION = None
+
+
+class BaseClient(rest_client.RestClient):
+    """Base volume service clients class to support microversion."""
+    api_microversion_header_name = 'Openstack-Api-Version'
+
+    def get_headers(self, accept_type=None, send_type=None):
+        headers = super(BaseClient, self).get_headers(
+            accept_type=accept_type, send_type=send_type)
+        if VOLUME_MICROVERSION:
+            headers[self.api_microversion_header_name] = ('volume %s' %
+                                                          VOLUME_MICROVERSION)
+        return headers
+
+    def request(self, method, url, extra_headers=False, headers=None,
+                body=None, chunked=False):
+
+        resp, resp_body = super(BaseClient, self).request(
+            method, url, extra_headers, headers, body, chunked)
+        if (VOLUME_MICROVERSION and
+            VOLUME_MICROVERSION != api_version_utils.LATEST_MICROVERSION):
+            api_version_utils.assert_version_header_matches_request(
+                self.api_microversion_header_name,
+                'volume %s' % VOLUME_MICROVERSION,
+                resp)
+        return resp, resp_body
diff --git a/tempest/lib/services/volume/v2/encryption_types_client.py b/tempest/lib/services/volume/v2/encryption_types_client.py
index eeff537..20f3356 100755
--- a/tempest/lib/services/volume/v2/encryption_types_client.py
+++ b/tempest/lib/services/volume/v2/encryption_types_client.py
@@ -50,9 +50,9 @@
     def create_encryption_type(self, volume_type_id, **kwargs):
         """Create encryption type.
 
-        TODO: Current api-site doesn't contain this API description.
-        After fixing the api-site, we need to fix here also for putting
-        the link to api-site.
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v2/#create-an-encryption-type-for-v2
         """
         url = "/types/%s/encryption" % volume_type_id
         post_body = json.dumps({'encryption': kwargs})
@@ -71,9 +71,9 @@
     def update_encryption_type(self, volume_type_id, **kwargs):
         """Update an encryption type for an existing volume type.
 
-        TODO: Current api-site doesn't contain this API description.
-        After fixing the api-site, we need to fix here also for putting
-        the link to api-site.
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v2/#update-an-encryption-type-for-v2
         """
         url = "/types/%s/encryption/provider" % volume_type_id
         put_body = json.dumps({'encryption': kwargs})
diff --git a/tempest/lib/services/volume/v2/hosts_client.py b/tempest/lib/services/volume/v2/hosts_client.py
index 8fcf4d0..f44bda3 100644
--- a/tempest/lib/services/volume/v2/hosts_client.py
+++ b/tempest/lib/services/volume/v2/hosts_client.py
@@ -24,8 +24,12 @@
     api_version = "v2"
 
     def list_hosts(self, **params):
-        """Lists all hosts."""
+        """Lists all hosts.
 
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v2/#list-all-hosts
+        """
         url = 'os-hosts'
         if params:
             url += '?%s' % urllib.urlencode(params)
diff --git a/tempest/lib/services/volume/v2/snapshots_client.py b/tempest/lib/services/volume/v2/snapshots_client.py
index 983ed89..5f4e7de 100644
--- a/tempest/lib/services/volume/v2/snapshots_client.py
+++ b/tempest/lib/services/volume/v2/snapshots_client.py
@@ -124,7 +124,12 @@
         return rest_client.ResponseBody(resp, body)
 
     def create_snapshot_metadata(self, snapshot_id, metadata):
-        """Create metadata for the snapshot."""
+        """Create metadata for the snapshot.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        http://developer.openstack.org/api-ref/block-storage/v2/#create-snapshot-metadata
+        """
         put_body = json.dumps({'metadata': metadata})
         url = "snapshots/%s/metadata" % snapshot_id
         resp, body = self.post(url, put_body)
diff --git a/tempest/lib/services/volume/v2/volumes_client.py b/tempest/lib/services/volume/v2/volumes_client.py
index f4e7c6a..cfff16a 100644
--- a/tempest/lib/services/volume/v2/volumes_client.py
+++ b/tempest/lib/services/volume/v2/volumes_client.py
@@ -21,10 +21,11 @@
 
 from tempest.lib.common import rest_client
 from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.volume import base_client
 from tempest.lib.services.volume.v2 import transfers_client
 
 
-class VolumesClient(rest_client.RestClient):
+class VolumesClient(base_client.BaseClient):
     """Client class to send CRUD Volume V2 API requests"""
     api_version = "v2"
 
@@ -72,6 +73,10 @@
         """List all the volumes created.
 
         Params can be a string (must be urlencoded) or a dictionary.
+        For a full list of available parameters, please refer to the official
+        API reference:
+        http://developer.openstack.org/api-ref/block-storage/v2/#list-volumes-with-details
+        http://developer.openstack.org/api-ref/block-storage/v2/#list-volumes
         """
         url = 'volumes'
         if detail:
@@ -155,7 +160,12 @@
         return rest_client.ResponseBody(resp, body)
 
     def set_bootable_volume(self, volume_id, **kwargs):
-        """set a bootable flag for a volume - true or false."""
+        """Set a bootable flag for a volume - true or false.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        http://developer.openstack.org/api-ref/block-storage/v2/#update-volume-bootable-status
+        """
         post_body = json.dumps({'os-set_bootable': kwargs})
         url = 'volumes/%s/action' % (volume_id)
         resp, body = self.post(url, post_body)
@@ -239,7 +249,12 @@
         return rest_client.ResponseBody(resp, body)
 
     def create_volume_metadata(self, volume_id, metadata):
-        """Create metadata for the volume."""
+        """Create metadata for the volume.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        http://developer.openstack.org/api-ref/block-storage/v2/#create-volume-metadata
+        """
         put_body = json.dumps({'metadata': metadata})
         url = "volumes/%s/metadata" % volume_id
         resp, body = self.post(url, put_body)
@@ -256,7 +271,12 @@
         return rest_client.ResponseBody(resp, body)
 
     def update_volume_metadata(self, volume_id, metadata):
-        """Update metadata for the volume."""
+        """Update metadata for the volume.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        http://developer.openstack.org/api-ref/block-storage/v2/#update-volume-metadata
+        """
         put_body = json.dumps({'metadata': metadata})
         url = "volumes/%s/metadata" % volume_id
         resp, body = self.put(url, put_body)
@@ -281,7 +301,12 @@
         return rest_client.ResponseBody(resp, body)
 
     def retype_volume(self, volume_id, **kwargs):
-        """Updates volume with new volume type."""
+        """Updates volume with new volume type.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v2/#retype-volume
+        """
         post_body = json.dumps({'os-retype': kwargs})
         resp, body = self.post('volumes/%s/action' % volume_id, post_body)
         self.expected_success(202, resp.status)
diff --git a/tempest/lib/services/volume/v3/__init__.py b/tempest/lib/services/volume/v3/__init__.py
index 07ae917..a351d61 100644
--- a/tempest/lib/services/volume/v3/__init__.py
+++ b/tempest/lib/services/volume/v3/__init__.py
@@ -13,8 +13,11 @@
 # the License.
 
 from tempest.lib.services.volume.v3.base_client import BaseClient
+from tempest.lib.services.volume.v3.group_types_client import GroupTypesClient
+from tempest.lib.services.volume.v3.groups_client import GroupsClient
 from tempest.lib.services.volume.v3.messages_client import MessagesClient
 from tempest.lib.services.volume.v3.versions_client import VersionsClient
 from tempest.lib.services.volume.v3.volumes_client import VolumesClient
 
-__all__ = ['MessagesClient', 'BaseClient', 'VersionsClient', 'VolumesClient']
+__all__ = ['BaseClient', 'GroupsClient', 'GroupTypesClient',
+           'MessagesClient', 'VersionsClient', 'VolumesClient']
diff --git a/tempest/lib/services/volume/v3/base_client.py b/tempest/lib/services/volume/v3/base_client.py
index 958212a..e78380b 100644
--- a/tempest/lib/services/volume/v3/base_client.py
+++ b/tempest/lib/services/volume/v3/base_client.py
@@ -13,34 +13,11 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from tempest.lib.common import api_version_utils
-from tempest.lib.common import rest_client
+from debtcollector import moves
 
-VOLUME_MICROVERSION = None
+from tempest.lib.services.volume import base_client
 
 
-class BaseClient(rest_client.RestClient):
-    """Base class to handle Cinder v3 client microversion support."""
-    api_version = 'v3'
-    api_microversion_header_name = 'Openstack-Api-Version'
-
-    def get_headers(self, accept_type=None, send_type=None):
-        headers = super(BaseClient, self).get_headers(
-            accept_type=accept_type, send_type=send_type)
-        if VOLUME_MICROVERSION:
-            headers[self.api_microversion_header_name] = ('volume %s' %
-                                                          VOLUME_MICROVERSION)
-        return headers
-
-    def request(self, method, url, extra_headers=False, headers=None,
-                body=None, chunked=False):
-
-        resp, resp_body = super(BaseClient, self).request(
-            method, url, extra_headers, headers, body, chunked)
-        if (VOLUME_MICROVERSION and
-            VOLUME_MICROVERSION != api_version_utils.LATEST_MICROVERSION):
-            api_version_utils.assert_version_header_matches_request(
-                self.api_microversion_header_name,
-                'volume %s' % VOLUME_MICROVERSION,
-                resp)
-        return resp, resp_body
+BaseClient = moves.moved_class(base_client.BaseClient, 'BaseClient', __name__,
+                               version="Pike", removal_version='?')
+BaseClient.api_version = 'v3'
diff --git a/tempest/lib/services/volume/v3/group_types_client.py b/tempest/lib/services/volume/v3/group_types_client.py
new file mode 100644
index 0000000..a6edbf5
--- /dev/null
+++ b/tempest/lib/services/volume/v3/group_types_client.py
@@ -0,0 +1,48 @@
+# Copyright (C) 2017 Dell Inc. or its subsidiaries.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+from tempest.lib.services.volume import base_client
+
+
+class GroupTypesClient(base_client.BaseClient):
+    """Client class to send CRUD Volume V3 Group Types API requests"""
+    api_version = 'v3'
+
+    @property
+    def resource_type(self):
+        """Returns the primary type of resource this client works with."""
+        return 'group-type'
+
+    def create_group_type(self, **kwargs):
+        """Create group_type.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v3/#create-group-type
+        """
+        post_body = json.dumps({'group_type': kwargs})
+        resp, body = self.post('group_types', post_body)
+        body = json.loads(body)
+        self.expected_success(202, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def delete_group_type(self, group_type_id):
+        """Deletes the specified group_type."""
+        resp, body = self.delete("group_types/%s" % group_type_id)
+        self.expected_success(202, resp.status)
+        return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/groups_client.py b/tempest/lib/services/volume/v3/groups_client.py
new file mode 100644
index 0000000..9b53bb7
--- /dev/null
+++ b/tempest/lib/services/volume/v3/groups_client.py
@@ -0,0 +1,97 @@
+# Copyright (C) 2017 Dell Inc. or its subsidiaries.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.volume import base_client
+
+
+class GroupsClient(base_client.BaseClient):
+    """Client class to send CRUD Volume Group API requests"""
+    api_version = 'v3'
+
+    def create_group(self, **kwargs):
+        """Creates a group.
+
+        group_type and volume_types are required parameters in kwargs.
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v3/#create-group
+        """
+        post_body = json.dumps({'group': kwargs})
+        resp, body = self.post('groups', post_body)
+        body = json.loads(body)
+        self.expected_success(202, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def delete_group(self, group_id, delete_volumes=True):
+        """Deletes a group.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v3/#delete-group
+        """
+        post_body = {'delete-volumes': delete_volumes}
+        post_body = json.dumps({'delete': post_body})
+        resp, body = self.post('groups/%s/action' % group_id,
+                               post_body)
+        self.expected_success(202, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def show_group(self, group_id):
+        """Returns the details of a single group.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v3/#show-group-details
+        """
+        url = "groups/%s" % str(group_id)
+        resp, body = self.get(url)
+        body = json.loads(body)
+        self.expected_success(200, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def list_groups(self, detail=False, **params):
+        """Lists information for all the tenant's groups.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v3/#list-groups
+        https://developer.openstack.org/api-ref/block-storage/v3/#list-groups-with-details
+        """
+        url = "groups"
+        if detail:
+            url += "/detail"
+        if params:
+            url += '?%s' % urllib.urlencode(params)
+        resp, body = self.get(url)
+        body = json.loads(body)
+        self.expected_success(200, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def is_resource_deleted(self, id):
+        try:
+            self.show_group(id)
+        except lib_exc.NotFound:
+            return True
+        return False
+
+    @property
+    def resource_type(self):
+        """Returns the primary type of resource this client works with."""
+        return 'group'
diff --git a/tempest/lib/services/volume/v3/messages_client.py b/tempest/lib/services/volume/v3/messages_client.py
index 8a01864..0127271 100644
--- a/tempest/lib/services/volume/v3/messages_client.py
+++ b/tempest/lib/services/volume/v3/messages_client.py
@@ -17,11 +17,12 @@
 
 from tempest.lib.common import rest_client
 from tempest.lib import exceptions as lib_exc
-from tempest.lib.services.volume.v3 import base_client
+from tempest.lib.services.volume import base_client
 
 
 class MessagesClient(base_client.BaseClient):
     """Client class to send user messages API requests."""
+    api_version = 'v3'
 
     def show_message(self, message_id):
         """Show details for a single message."""
diff --git a/tempest/lib/services/volume/v3/versions_client.py b/tempest/lib/services/volume/v3/versions_client.py
index e2941c4..5702f95 100644
--- a/tempest/lib/services/volume/v3/versions_client.py
+++ b/tempest/lib/services/volume/v3/versions_client.py
@@ -18,10 +18,11 @@
 
 from tempest.lib.api_schema.response.volume import versions as schema
 from tempest.lib.common import rest_client
-from tempest.lib.services.volume.v3 import base_client
+from tempest.lib.services.volume import base_client
 
 
 class VersionsClient(base_client.BaseClient):
+    api_version = 'v3'
 
     def list_versions(self):
         """List API versions
diff --git a/tempest/lib/services/volume/v3/volumes_client.py b/tempest/lib/services/volume/v3/volumes_client.py
index aec0205..5f4b278 100644
--- a/tempest/lib/services/volume/v3/volumes_client.py
+++ b/tempest/lib/services/volume/v3/volumes_client.py
@@ -18,11 +18,9 @@
 
 from tempest.lib.common import rest_client
 from tempest.lib.services.volume.v2 import volumes_client
-from tempest.lib.services.volume.v3 import base_client
 
 
-class VolumesClient(base_client.BaseClient,
-                    volumes_client.VolumesClient):
+class VolumesClient(volumes_client.VolumesClient):
     """Client class to send CRUD Volume V3 API requests"""
     api_version = "v3"
 
diff --git a/tempest/tests/cmd/test_verify_tempest_config.py b/tempest/tests/cmd/test_verify_tempest_config.py
index 640dcd4..b0e74fb 100644
--- a/tempest/tests/cmd/test_verify_tempest_config.py
+++ b/tempest/tests/cmd/test_verify_tempest_config.py
@@ -199,7 +199,9 @@
         with mock.patch.object(verify_tempest_config,
                                'print_and_or_update') as print_mock:
             verify_tempest_config.verify_cinder_api_versions(fake_os, True)
-        print_mock.assert_not_called()
+        print_mock.assert_any_call('api_v3', 'volume-feature-enabled',
+                                   False, True)
+        self.assertEqual(1, print_mock.call_count)
 
     @mock.patch('tempest.lib.common.http.ClosingHttp.request')
     def test_verify_cinder_api_versions_no_v2(self, mock_request):
@@ -215,9 +217,7 @@
             verify_tempest_config.verify_cinder_api_versions(fake_os, True)
         print_mock.assert_any_call('api_v2', 'volume-feature-enabled',
                                    False, True)
-        print_mock.assert_any_call('api_v3', 'volume-feature-enabled',
-                                   True, True)
-        self.assertEqual(2, print_mock.call_count)
+        self.assertEqual(1, print_mock.call_count)
 
     @mock.patch('tempest.lib.common.http.ClosingHttp.request')
     def test_verify_cinder_api_versions_no_v1(self, mock_request):
@@ -231,9 +231,7 @@
         with mock.patch.object(verify_tempest_config,
                                'print_and_or_update') as print_mock:
             verify_tempest_config.verify_cinder_api_versions(fake_os, True)
-        print_mock.assert_any_call('api_v3', 'volume-feature-enabled',
-                                   True, True)
-        self.assertEqual(1, print_mock.call_count)
+        print_mock.assert_not_called()
 
     def test_verify_glance_version_no_v2_with_v1_1(self):
         def fake_get_versions():
diff --git a/tempest/tests/cmd/test_workspace.py b/tempest/tests/cmd/test_workspace.py
index dc6c0c8..a1c8c53 100644
--- a/tempest/tests/cmd/test_workspace.py
+++ b/tempest/tests/cmd/test_workspace.py
@@ -80,13 +80,20 @@
         self.assertEqual(
             self.workspace_manager.get_workspace(self.name), new_path)
 
-    def test_run_workspace_remove(self):
+    def test_run_workspace_remove_entry(self):
         cmd = ['tempest', 'workspace', 'remove',
                '--workspace-path', self.store_file,
                '--name', self.name]
         self._run_cmd_gets_return_code(cmd, 0)
         self.assertIsNone(self.workspace_manager.get_workspace(self.name))
 
+    def test_run_workspace_remove_directory(self):
+        cmd = ['tempest', 'workspace', 'remove',
+               '--workspace-path', self.store_file,
+               '--name', self.name, '--rmdir']
+        self._run_cmd_gets_return_code(cmd, 0)
+        self.assertIsNone(self.workspace_manager.get_workspace(self.name))
+
 
 class TestTempestWorkspaceManager(TestTempestWorkspaceBase):
     def setUp(self):
@@ -117,8 +124,13 @@
         self.assertEqual(
             self.workspace_manager.get_workspace(self.name), new_path)
 
-    def test_workspace_manager_remove(self):
-        self.workspace_manager.remove_workspace(self.name)
+    def test_workspace_manager_remove_entry(self):
+        self.workspace_manager.remove_workspace_entry(self.name)
+        self.assertIsNone(self.workspace_manager.get_workspace(self.name))
+
+    def test_workspace_manager_remove_directory(self):
+        path = self.workspace_manager.remove_workspace_entry(self.name)
+        self.workspace_manager.remove_workspace_directory(path)
         self.assertIsNone(self.workspace_manager.get_workspace(self.name))
 
     def test_path_expansion(self):
diff --git a/tempest/tests/common/test_compute.py b/tempest/tests/common/test_compute.py
new file mode 100644
index 0000000..c108be9
--- /dev/null
+++ b/tempest/tests/common/test_compute.py
@@ -0,0 +1,106 @@
+# Copyright 2017 Citrix Systems
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from six.moves.urllib import parse as urlparse
+
+import mock
+
+from tempest.common import compute
+from tempest.tests import base
+
+
+class TestCompute(base.TestCase):
+    def setUp(self):
+        super(TestCompute, self).setUp()
+        self.client_sock = mock.Mock()
+        self.url = urlparse.urlparse("http://www.fake.com:80")
+
+    def test_rfp_frame_not_cached(self):
+        # rfp negotiation frame arrived separately after upgrade
+        # response, so it's not cached.
+        RFP_VERSION = b'RFB.003.003\x0a'
+        rfp_frame_header = b'\x82\x0c'
+
+        self.client_sock.recv.side_effect = [
+            b'fake response start\r\n',
+            b'fake response end\r\n\r\n',
+            rfp_frame_header,
+            RFP_VERSION]
+        expect_response = b'fake response start\r\nfake response end\r\n\r\n'
+
+        webSocket = compute._WebSocket(self.client_sock, self.url)
+
+        self.assertEqual(webSocket.response, expect_response)
+        # no cache
+        self.assertEqual(webSocket.cached_stream, b'')
+        self.client_sock.recv.assert_has_calls([mock.call(4096),
+                                                mock.call(4096)])
+
+        self.client_sock.recv.reset_mock()
+        recv_version = webSocket.receive_frame()
+
+        self.assertEqual(recv_version, RFP_VERSION)
+        self.client_sock.recv.assert_has_calls([mock.call(2),
+                                                mock.call(12)])
+
+    def test_rfp_frame_fully_cached(self):
+        RFP_VERSION = b'RFB.003.003\x0a'
+        rfp_version_frame = b'\x82\x0c%s' % RFP_VERSION
+
+        self.client_sock.recv.side_effect = [
+            b'fake response start\r\n',
+            b'fake response end\r\n\r\n%s' % rfp_version_frame]
+        expect_response = b'fake response start\r\nfake response end\r\n\r\n'
+        webSocket = compute._WebSocket(self.client_sock, self.url)
+
+        self.client_sock.recv.assert_has_calls([mock.call(4096),
+                                                mock.call(4096)])
+        self.assertEqual(webSocket.response, expect_response)
+        self.assertEqual(webSocket.cached_stream, rfp_version_frame)
+
+        self.client_sock.recv.reset_mock()
+        recv_version = webSocket.receive_frame()
+
+        self.client_sock.recv.assert_not_called()
+        self.assertEqual(recv_version, RFP_VERSION)
+        # cached_stream should be empty in the end.
+        self.assertEqual(webSocket.cached_stream, b'')
+
+    def test_rfp_frame_partially_cached(self):
+        RFP_VERSION = b'RFB.003.003\x0a'
+        rfp_version_frame = b'\x82\x0c%s' % RFP_VERSION
+        frame_part1 = rfp_version_frame[:6]
+        frame_part2 = rfp_version_frame[6:]
+
+        self.client_sock.recv.side_effect = [
+            b'fake response start\r\n',
+            b'fake response end\r\n\r\n%s' % frame_part1,
+            frame_part2]
+        expect_response = b'fake response start\r\nfake response end\r\n\r\n'
+        webSocket = compute._WebSocket(self.client_sock, self.url)
+
+        self.client_sock.recv.assert_has_calls([mock.call(4096),
+                                                mock.call(4096)])
+        self.assertEqual(webSocket.response, expect_response)
+        self.assertEqual(webSocket.cached_stream, frame_part1)
+
+        self.client_sock.recv.reset_mock()
+
+        recv_version = webSocket.receive_frame()
+
+        self.client_sock.recv.assert_called_once_with(len(frame_part2))
+        self.assertEqual(recv_version, RFP_VERSION)
+        # cached_stream should be empty in the end.
+        self.assertEqual(webSocket.cached_stream, b'')
diff --git a/tempest/tests/lib/services/base.py b/tempest/tests/lib/services/base.py
index 90c9f63..778c966 100644
--- a/tempest/tests/lib/services/base.py
+++ b/tempest/tests/lib/services/base.py
@@ -40,7 +40,7 @@
                function.
         :param body: Expected response body returned by the service client
                function.
-        :param to_utf: Whether to use UTF-8 encoding for request.
+        :param to_utf: Whether to use UTF-8 encoding for response.
         :param status: Expected response status returned by the service client
                function.
         :param headers: Expected headers returned by the service client
diff --git a/tempest/tests/lib/services/compute/test_servers_client.py b/tempest/tests/lib/services/compute/test_servers_client.py
index a857329..86f6ad5 100644
--- a/tempest/tests/lib/services/compute/test_servers_client.py
+++ b/tempest/tests/lib/services/compute/test_servers_client.py
@@ -235,7 +235,7 @@
             server_id=self.server_id
             )
 
-    def test_delete_server(self, bytes_body=False):
+    def test_delete_server(self):
         self.check_service_client_function(
             self.client.delete_server,
             'tempest.lib.common.rest_client.RestClient.delete',
@@ -288,6 +288,7 @@
             self.client.list_addresses_by_network,
             'tempest.lib.common.rest_client.RestClient.get',
             self.FAKE_ADDRESS['addresses'],
+            bytes_body,
             server_id=self.server_id,
             network_id=self.network_id
             )
@@ -303,6 +304,7 @@
             self.client.action,
             'tempest.lib.common.rest_client.RestClient.post',
             {},
+            bytes_body,
             server_id=self.server_id,
             action_name='fake-action-name',
             schema={'status_code': 200}
@@ -319,6 +321,7 @@
             self.client.create_backup,
             'tempest.lib.common.rest_client.RestClient.post',
             {},
+            bytes_body,
             status=202,
             server_id=self.server_id,
             backup_type='fake-backup',
@@ -339,6 +342,7 @@
             self.client.evacuate_server,
             'tempest.lib.common.rest_client.RestClient.post',
             self.FAKE_SERVER_PASSWORD,
+            bytes_body,
             **kwargs)
 
     def test_change_password_with_str_body(self):
@@ -352,6 +356,7 @@
             self.client.change_password,
             'tempest.lib.common.rest_client.RestClient.post',
             {},
+            bytes_body,
             status=202,
             server_id=self.server_id,
             adminPass='fake-admin-pass'
@@ -368,16 +373,11 @@
             self.client.show_password,
             'tempest.lib.common.rest_client.RestClient.get',
             {'password': 'fake-password'},
+            bytes_body,
             server_id=self.server_id
             )
 
-    def test_delete_password_with_str_body(self):
-        self._test_delete_password()
-
-    def test_delete_password_with_bytes_body(self):
-        self._test_delete_password(True)
-
-    def _test_delete_password(self, bytes_body=False):
+    def test_delete_password(self):
         self.check_service_client_function(
             self.client.delete_password,
             'tempest.lib.common.rest_client.RestClient.delete',
@@ -386,13 +386,7 @@
             server_id=self.server_id
             )
 
-    def test_reboot_server_with_str_body(self):
-        self._test_reboot_server()
-
-    def test_reboot_server_with_bytes_body(self):
-        self._test_reboot_server(True)
-
-    def _test_reboot_server(self, bytes_body=False):
+    def test_reboot_server(self):
         self.check_service_client_function(
             self.client.reboot_server,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -413,18 +407,13 @@
             self.client.rebuild_server,
             'tempest.lib.common.rest_client.RestClient.post',
             self.FAKE_REBUILD_SERVER,
+            bytes_body,
             status=202,
             server_id=self.server_id,
             image_ref='fake-image-ref'
             )
 
-    def test_resize_server_with_str_body(self):
-        self._test_resize_server()
-
-    def test_resize_server_with_bytes_body(self):
-        self._test_resize_server(True)
-
-    def _test_resize_server(self, bytes_body=False):
+    def test_resize_server(self):
         self.check_service_client_function(
             self.client.resize_server,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -434,13 +423,7 @@
             flavor_ref='fake-flavor-ref'
             )
 
-    def test_confirm_resize_server_with_str_body(self):
-        self._test_confirm_resize_server()
-
-    def test_confirm_resize_server_with_bytes_body(self):
-        self._test_confirm_resize_server(True)
-
-    def _test_confirm_resize_server(self, bytes_body=False):
+    def test_confirm_resize_server(self):
         self.check_service_client_function(
             self.client.confirm_resize_server,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -449,13 +432,7 @@
             server_id=self.server_id
             )
 
-    def test_revert_resize_server_with_str_body(self):
-        self._test_revert_resize()
-
-    def test_revert_resize_server_with_bytes_body(self):
-        self._test_revert_resize(True)
-
-    def _test_revert_resize(self, bytes_body=False):
+    def test_revert_resize(self):
         self.check_service_client_function(
             self.client.revert_resize_server,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -475,6 +452,7 @@
             self.client.list_server_metadata,
             'tempest.lib.common.rest_client.RestClient.get',
             {'metadata': {'fake-key': 'fake-meta-data'}},
+            bytes_body,
             server_id=self.server_id
             )
 
@@ -489,6 +467,7 @@
             self.client.set_server_metadata,
             'tempest.lib.common.rest_client.RestClient.put',
             {'metadata': {'fake-key': 'fake-meta-data'}},
+            bytes_body,
             server_id=self.server_id,
             meta='fake-meta'
             )
@@ -504,6 +483,7 @@
             self.client.update_server_metadata,
             'tempest.lib.common.rest_client.RestClient.post',
             {'metadata': {'fake-key': 'fake-meta-data'}},
+            bytes_body,
             server_id=self.server_id,
             meta='fake-meta'
             )
@@ -519,6 +499,7 @@
             self.client.show_server_metadata_item,
             'tempest.lib.common.rest_client.RestClient.get',
             {'meta': {'fake-key': 'fake-meta-data'}},
+            bytes_body,
             server_id=self.server_id,
             key='fake-key'
             )
@@ -534,18 +515,13 @@
             self.client.set_server_metadata_item,
             'tempest.lib.common.rest_client.RestClient.put',
             {'meta': {'fake-key': 'fake-meta-data'}},
+            bytes_body,
             server_id=self.server_id,
             key='fake-key',
             meta='fake-meta'
             )
 
-    def test_delete_server_metadata_item_with_str_body(self):
-        self._test_delete_server_metadata()
-
-    def test_delete_server_metadata_item_with_bytes_body(self):
-        self._test_delete_server_metadata(True)
-
-    def _test_delete_server_metadata(self, bytes_body=False):
+    def test_delete_server_metadata(self):
         self.check_service_client_function(
             self.client.delete_server_metadata_item,
             'tempest.lib.common.rest_client.RestClient.delete',
@@ -555,13 +531,7 @@
             key='fake-key'
             )
 
-    def test_stop_server_with_str_body(self):
-        self._test_stop_server()
-
-    def test_stop_server_with_bytes_body(self):
-        self._test_stop_server(True)
-
-    def _test_stop_server(self, bytes_body=False):
+    def test_stop_server(self):
         self.check_service_client_function(
             self.client.stop_server,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -570,13 +540,7 @@
             server_id=self.server_id
             )
 
-    def test_start_server_with_str_body(self):
-        self._test_start_server()
-
-    def test_start_server_with_bytes_body(self):
-        self._test_start_server(True)
-
-    def _test_start_server(self, bytes_body=False):
+    def test_start_server(self):
         self.check_service_client_function(
             self.client.start_server,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -596,6 +560,7 @@
             self.client.attach_volume,
             'tempest.lib.common.rest_client.RestClient.post',
             {'volumeAttachment': self.FAKE_COMMON_VOLUME},
+            bytes_body,
             server_id=self.server_id
             )
 
@@ -621,6 +586,7 @@
             self.client.detach_volume,
             'tempest.lib.common.rest_client.RestClient.delete',
             {},
+            bytes_body,
             status=202,
             server_id=self.server_id,
             volume_id=self.FAKE_COMMON_VOLUME['volumeId']
@@ -637,6 +603,7 @@
             self.client.show_volume_attachment,
             'tempest.lib.common.rest_client.RestClient.get',
             {'volumeAttachment': self.FAKE_COMMON_VOLUME},
+            bytes_body,
             server_id=self.server_id,
             volume_id=self.FAKE_COMMON_VOLUME['volumeId']
             )
@@ -652,6 +619,7 @@
             self.client.list_volume_attachments,
             'tempest.lib.common.rest_client.RestClient.get',
             {'volumeAttachments': [self.FAKE_COMMON_VOLUME]},
+            bytes_body,
             server_id=self.server_id
             )
 
@@ -666,18 +634,13 @@
             self.client.add_security_group,
             'tempest.lib.common.rest_client.RestClient.post',
             {},
+            bytes_body,
             status=202,
             server_id=self.server_id,
             name='fake-name'
             )
 
-    def test_remove_security_group_with_str_body(self):
-        self._test_remove_security_group()
-
-    def test_remove_security_group_with_bytes_body(self):
-        self._test_remove_security_group(True)
-
-    def _test_remove_security_group(self, bytes_body=False):
+    def test_remove_security_group(self):
         self.check_service_client_function(
             self.client.remove_security_group,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -698,6 +661,7 @@
             self.client.live_migrate_server,
             'tempest.lib.common.rest_client.RestClient.post',
             {},
+            bytes_body,
             status=202,
             server_id=self.server_id
             )
@@ -713,17 +677,12 @@
             self.client.migrate_server,
             'tempest.lib.common.rest_client.RestClient.post',
             {},
+            bytes_body,
             status=202,
             server_id=self.server_id
             )
 
-    def test_lock_server_with_str_body(self):
-        self._test_lock_server()
-
-    def test_lock_server_with_bytes_body(self):
-        self._test_lock_server(True)
-
-    def _test_lock_server(self, bytes_body=False):
+    def test_lock_server(self):
         self.check_service_client_function(
             self.client.lock_server,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -732,13 +691,7 @@
             server_id=self.server_id
             )
 
-    def test_unlock_server_with_str_body(self):
-        self._test_unlock_server()
-
-    def test_unlock_server_with_bytes_body(self):
-        self._test_unlock_server(True)
-
-    def _test_unlock_server(self, bytes_body=False):
+    def test_unlock_server(self):
         self.check_service_client_function(
             self.client.unlock_server,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -747,13 +700,7 @@
             server_id=self.server_id
             )
 
-    def test_suspend_server_with_str_body(self):
-        self._test_suspend_server()
-
-    def test_suspend_server_with_bytes_body(self):
-        self._test_suspend_server(True)
-
-    def _test_suspend_server(self, bytes_body=False):
+    def test_suspend_server(self):
         self.check_service_client_function(
             self.client.suspend_server,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -762,13 +709,7 @@
             server_id=self.server_id
             )
 
-    def test_resume_server_with_str_body(self):
-        self._test_resume_server()
-
-    def test_resume_server_with_bytes_body(self):
-        self._test_resume_server(True)
-
-    def _test_resume_server(self, bytes_body=False):
+    def test_resume_server(self):
         self.check_service_client_function(
             self.client.resume_server,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -777,13 +718,7 @@
             server_id=self.server_id
             )
 
-    def test_pause_server_with_str_body(self):
-        self._test_pause_server()
-
-    def test_pause_server_with_bytes_body(self):
-        self._test_pause_server(True)
-
-    def _test_pause_server(self, bytes_body=False):
+    def test_pause_server(self):
         self.check_service_client_function(
             self.client.pause_server,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -792,13 +727,7 @@
             server_id=self.server_id
             )
 
-    def test_unpause_server_with_str_body(self):
-        self._test_unpause_server()
-
-    def test_unpause_server_with_bytes_body(self):
-        self._test_unpause_server(True)
-
-    def _test_unpause_server(self, bytes_body=False):
+    def test_unpause_server(self):
         self.check_service_client_function(
             self.client.unpause_server,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -807,13 +736,7 @@
             server_id=self.server_id
             )
 
-    def test_reset_state_with_str_body(self):
-        self._test_reset_state()
-
-    def test_reset_state_with_bytes_body(self):
-        self._test_reset_state(True)
-
-    def _test_reset_state(self, bytes_body=False):
+    def test_reset_state(self):
         self.check_service_client_function(
             self.client.reset_state,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -823,13 +746,7 @@
             state='fake-state'
             )
 
-    def test_shelve_server_with_str_body(self):
-        self._test_shelve_server()
-
-    def test_shelve_server_with_bytes_body(self):
-        self._test_shelve_server(True)
-
-    def _test_shelve_server(self, bytes_body=False):
+    def test_shelve_server(self):
         self.check_service_client_function(
             self.client.shelve_server,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -838,13 +755,7 @@
             server_id=self.server_id
             )
 
-    def test_unshelve_server_with_str_body(self):
-        self._test_unshelve_server()
-
-    def test_unshelve_server_with_bytes_body(self):
-        self._test_unshelve_server(True)
-
-    def _test_unshelve_server(self, bytes_body=False):
+    def test_unshelve_server(self):
         self.check_service_client_function(
             self.client.unshelve_server,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -853,13 +764,7 @@
             server_id=self.server_id
             )
 
-    def test_shelve_offload_server_with_str_body(self):
-        self._test_shelve_offload_server()
-
-    def test_shelve_offload_server_with_bytes_body(self):
-        self._test_shelve_offload_server(True)
-
-    def _test_shelve_offload_server(self, bytes_body=False):
+    def test_shelve_offload_server(self):
         self.check_service_client_function(
             self.client.shelve_offload_server,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -879,6 +784,7 @@
             self.client.get_console_output,
             'tempest.lib.common.rest_client.RestClient.post',
             {'output': 'fake-output'},
+            bytes_body,
             server_id=self.server_id,
             length='fake-length'
             )
@@ -894,6 +800,7 @@
             self.client.list_virtual_interfaces,
             'tempest.lib.common.rest_client.RestClient.get',
             {'virtual_interfaces': [self.FAKE_VIRTUAL_INTERFACES]},
+            bytes_body,
             server_id=self.server_id
             )
 
@@ -908,16 +815,11 @@
             self.client.rescue_server,
             'tempest.lib.common.rest_client.RestClient.post',
             {'adminPass': 'fake-admin-pass'},
+            bytes_body,
             server_id=self.server_id
             )
 
-    def test_unrescue_server_with_str_body(self):
-        self._test_unrescue_server()
-
-    def test_unrescue_server_with_bytes_body(self):
-        self._test_unrescue_server(True)
-
-    def _test_unrescue_server(self, bytes_body=False):
+    def test_unrescue_server(self):
         self.check_service_client_function(
             self.client.unrescue_server,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -937,6 +839,7 @@
             self.client.show_server_diagnostics,
             'tempest.lib.common.rest_client.RestClient.get',
             self.FAKE_SERVER_DIAGNOSTICS,
+            bytes_body,
             status=200,
             server_id=self.server_id
             )
@@ -952,6 +855,7 @@
             self.client.list_instance_actions,
             'tempest.lib.common.rest_client.RestClient.get',
             {'instanceActions': [self.FAKE_INSTANCE_ACTIONS]},
+            bytes_body,
             server_id=self.server_id
             )
 
@@ -966,17 +870,12 @@
             self.client.show_instance_action,
             'tempest.lib.common.rest_client.RestClient.get',
             {'instanceAction': self.FAKE_INSTANCE_WITH_EVENTS},
+            bytes_body,
             server_id=self.server_id,
             request_id='fake-request-id'
             )
 
-    def test_force_delete_server_with_str_body(self):
-        self._test_force_delete_server()
-
-    def test_force_delete_server_with_bytes_body(self):
-        self._test_force_delete_server(True)
-
-    def _test_force_delete_server(self, bytes_body=False):
+    def test_force_delete_server(self):
         self.check_service_client_function(
             self.client.force_delete_server,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -985,13 +884,7 @@
             server_id=self.server_id
             )
 
-    def test_restore_soft_deleted_server_with_str_body(self):
-        self._test_restore_soft_deleted_server()
-
-    def test_restore_soft_deleted_server_with_bytes_body(self):
-        self._test_restore_soft_deleted_server(True)
-
-    def _test_restore_soft_deleted_server(self, bytes_body=False):
+    def test_restore_soft_deleted_server(self):
         self.check_service_client_function(
             self.client.restore_soft_deleted_server,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -1000,13 +893,7 @@
             server_id=self.server_id
             )
 
-    def test_reset_network_with_str_body(self):
-        self._test_reset_network()
-
-    def test_reset_network_with_bytes_body(self):
-        self._test_reset_network(True)
-
-    def _test_reset_network(self, bytes_body=False):
+    def test_reset_network(self):
         self.check_service_client_function(
             self.client.reset_network,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -1015,13 +902,7 @@
             server_id=self.server_id
             )
 
-    def test_inject_network_info_with_str_body(self):
-        self._test_inject_network_info()
-
-    def test_inject_network_info_with_bytes_body(self):
-        self._test_inject_network_info(True)
-
-    def _test_inject_network_info(self, bytes_body=False):
+    def test_inject_network_info(self):
         self.check_service_client_function(
             self.client.inject_network_info,
             'tempest.lib.common.rest_client.RestClient.post',
@@ -1041,6 +922,7 @@
             self.client.get_vnc_console,
             'tempest.lib.common.rest_client.RestClient.post',
             {'console': self.FAKE_VNC_CONSOLE},
+            bytes_body,
             server_id=self.server_id,
             type='fake-console-type'
             )
@@ -1056,7 +938,8 @@
             self.client.list_security_groups_by_server,
             'tempest.lib.common.rest_client.RestClient.get',
             {'security_groups': self.FAKE_SECURITY_GROUPS},
-            server_id=self.server_id,
+            bytes_body,
+            server_id=self.server_id
             )
 
     @mock.patch.object(base_compute_client, 'COMPUTE_MICROVERSION',
@@ -1151,15 +1034,7 @@
 
     @mock.patch.object(base_compute_client, 'COMPUTE_MICROVERSION',
                        new_callable=mock.PropertyMock(return_value='2.26'))
-    def test_delete_tag_str_body(self, _):
-        self._test_delete_tag()
-
-    @mock.patch.object(base_compute_client, 'COMPUTE_MICROVERSION',
-                       new_callable=mock.PropertyMock(return_value='2.26'))
-    def test_delete_tag_byte_body(self, _):
-        self._test_delete_tag(bytes_body=True)
-
-    def _test_delete_tag(self, bytes_body=False):
+    def test_delete_tag(self, _):
         self.check_service_client_function(
             self.client.delete_tag,
             'tempest.lib.common.rest_client.RestClient.delete',
@@ -1167,7 +1042,7 @@
             server_id=self.server_id,
             tag=self.FAKE_TAGS[0],
             status=204,
-            to_utf=bytes_body)
+            )
 
 
 class TestServersClientMinV26(base.BaseServiceTest):
diff --git a/tempest/tests/lib/services/identity/v3/test_endpoints_client.py b/tempest/tests/lib/services/identity/v3/test_endpoints_client.py
index f8c553f..ca15dd1 100644
--- a/tempest/tests/lib/services/identity/v3/test_endpoints_client.py
+++ b/tempest/tests/lib/services/identity/v3/test_endpoints_client.py
@@ -53,6 +53,8 @@
         ]
     }
 
+    FAKE_SERVICE_ID = "a4dc5060-f757-4662-b658-edd2aefbb41d"
+
     def setUp(self):
         super(TestEndpointsClient, self).setUp()
         fake_auth = fake_auth_provider.FakeAuthProvider()
@@ -72,12 +74,15 @@
             adminurl="https://compute.north.internal.com/v1",
             internalurl="https://compute.north.internal.com/v1")
 
-    def _test_list_endpoints(self, bytes_body=False):
+    def _test_list_endpoints(self, bytes_body=False, mock_args='endpoints',
+                             **params):
         self.check_service_client_function(
             self.client.list_endpoints,
             'tempest.lib.common.rest_client.RestClient.get',
             self.FAKE_LIST_ENDPOINTS,
-            bytes_body)
+            bytes_body,
+            mock_args=[mock_args],
+            **params)
 
     def test_create_endpoint_with_str_body(self):
         self._test_create_endpoint()
@@ -91,6 +96,16 @@
     def test_list_endpoints_with_bytes_body(self):
         self._test_list_endpoints(bytes_body=True)
 
+    def test_list_endpoints_with_params(self):
+        # Run the test separately for each param, to avoid assertion error
+        # resulting from randomized params order.
+        mock_args = 'endpoints?service_id=%s' % self.FAKE_SERVICE_ID
+        self._test_list_endpoints(mock_args=mock_args,
+                                  service_id=self.FAKE_SERVICE_ID)
+
+        mock_args = 'endpoints?interface=public'
+        self._test_list_endpoints(mock_args=mock_args, interface='public')
+
     def test_delete_endpoint(self):
         self.check_service_client_function(
             self.client.delete_endpoint,
diff --git a/tempest/tests/lib/services/identity/v3/test_oauth_token_client.py b/tempest/tests/lib/services/identity/v3/test_oauth_token_client.py
new file mode 100644
index 0000000..b9b9b15
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_oauth_token_client.py
@@ -0,0 +1,215 @@
+# Copyright 2017 AT&T 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.
+
+from oslotest import mockpatch
+
+from tempest.lib.services.identity.v3 import oauth_token_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib import fake_http
+from tempest.tests.lib.services import base
+
+
+class TestOAUTHTokenClient(base.BaseServiceTest):
+    FAKE_CREATE_REQUEST_TOKEN = {
+        'oauth_token': '29971f',
+        'oauth_token_secret': '238eb8',
+        'oauth_expires_at': '2013-09-11T06:07:51.501805Z'
+    }
+
+    FAKE_AUTHORIZE_REQUEST_TOKEN = {
+        'token': {
+            'oauth_verifier': '8171'
+        }
+    }
+
+    FAKE_CREATE_ACCESS_TOKEN = {
+        'oauth_token': 'accd36',
+        'oauth_token_secret': 'aa47da',
+        'oauth_expires_at': '2013-09-11T06:07:51.501805Z'
+    }
+
+    FAKE_ACCESS_TOKEN_INFO = {
+        'access_token': {
+            'consumer_id': '7fea2d',
+            'id': '6be26a',
+            'expires_at': '2013-09-11T06:07:51.501805Z',
+            'links': {
+                'roles': 'http://example.com/identity/v3/' +
+                         'users/ce9e07/OS-OAUTH1/access_tokens/6be26a/roles',
+                'self': 'http://example.com/identity/v3/' +
+                        'users/ce9e07/OS-OAUTH1/access_tokens/6be26a'
+            },
+            'project_id': 'b9fca3',
+            'authorizing_user_id': 'ce9e07'
+        }
+    }
+
+    FAKE_LIST_ACCESS_TOKENS = {
+        'access_tokens': [
+            {
+                'consumer_id': '7fea2d',
+                'id': '6be26a',
+                'expires_at': '2013-09-11T06:07:51.501805Z',
+                'links': {
+                    'roles': 'http://example.com/identity/v3/' +
+                             'users/ce9e07/OS-OAUTH1/access_tokens/' +
+                             '6be26a/roles',
+                    'self': 'http://example.com/identity/v3/' +
+                            'users/ce9e07/OS-OAUTH1/access_tokens/6be26a'
+                },
+                'project_id': 'b9fca3',
+                'authorizing_user_id': 'ce9e07'
+            }
+        ],
+        'links': {
+            'next': None,
+            'previous': None,
+            'self': 'http://example.com/identity/v3/' +
+                    'users/ce9e07/OS-OAUTH1/access_tokens'
+        }
+    }
+
+    FAKE_LIST_ACCESS_TOKEN_ROLES = {
+        'roles': [
+            {
+                'id': '26b860',
+                'domain_id': 'fake_domain',
+                'links': {
+                    'self': 'http://example.com/identity/v3/' +
+                            'roles/26b860'
+                },
+                'name': 'fake_role'
+            }
+        ],
+        'links': {
+            'next': None,
+            'previous': None,
+            'self': 'http://example.com/identity/v3/' +
+                    'users/ce9e07/OS-OAUTH1/access_tokens/6be26a/roles'
+        }
+    }
+
+    FAKE_ACCESS_TOKEN_ROLE_INFO = {
+        'role': {
+            'id': '26b860',
+            'domain_id': 'fake_domain',
+            'links': {
+                'self': 'http://example.com/identity/v3/' +
+                        'roles/26b860'
+            },
+            'name': 'fake_role'
+        }
+    }
+
+    def setUp(self):
+        super(TestOAUTHTokenClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = oauth_token_client.OAUTHTokenClient(fake_auth,
+                                                          'identity',
+                                                          'regionOne')
+
+    def _mock_token_response(self, body):
+        temp_response = [key + '=' + value for key, value in body.items()]
+        return '&'.join(temp_response)
+
+    def _test_authorize_request_token(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.authorize_request_token,
+            'tempest.lib.common.rest_client.RestClient.put',
+            self.FAKE_AUTHORIZE_REQUEST_TOKEN,
+            bytes_body,
+            request_token_id=self.FAKE_CREATE_REQUEST_TOKEN['oauth_token'],
+            role_ids=['26b860'],
+            status=200)
+
+    def test_create_request_token(self):
+        mock_resp = self._mock_token_response(self.FAKE_CREATE_REQUEST_TOKEN)
+        resp = fake_http.fake_http_response(None, status=201), mock_resp
+        self.useFixture(mockpatch.Patch(
+            'tempest.lib.common.rest_client.RestClient.post',
+            return_value=resp))
+
+        resp = self.client.create_request_token(
+            consumer_key='12345',
+            consumer_secret='23456',
+            project_id='c8f58432c6f00162f04d3250f')
+        self.assertEqual(self.FAKE_CREATE_REQUEST_TOKEN, resp)
+
+    def test_authorize_token_request_with_str_body(self):
+        self._test_authorize_request_token()
+
+    def test_authorize_token_request_with_bytes_body(self):
+        self._test_authorize_request_token(bytes_body=True)
+
+    def test_create_access_token(self):
+        mock_resp = self._mock_token_response(self.FAKE_CREATE_ACCESS_TOKEN)
+        req_secret = self.FAKE_CREATE_REQUEST_TOKEN['oauth_token_secret']
+        resp = fake_http.fake_http_response(None, status=201), mock_resp
+        self.useFixture(mockpatch.Patch(
+            'tempest.lib.common.rest_client.RestClient.post',
+            return_value=resp))
+
+        resp = self.client.create_access_token(
+            consumer_key='12345',
+            consumer_secret='23456',
+            request_key=self.FAKE_CREATE_REQUEST_TOKEN['oauth_token'],
+            request_secret=req_secret,
+            oauth_verifier='8171')
+        self.assertEqual(self.FAKE_CREATE_ACCESS_TOKEN, resp)
+
+    def test_get_access_token(self):
+        self.check_service_client_function(
+            self.client.get_access_token,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_ACCESS_TOKEN_INFO,
+            user_id='ce9e07',
+            access_token_id=self.FAKE_ACCESS_TOKEN_INFO['access_token']['id'],
+            status=200)
+
+    def test_list_access_tokens(self):
+        self.check_service_client_function(
+            self.client.list_access_tokens,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_LIST_ACCESS_TOKENS,
+            user_id='ce9e07',
+            status=200)
+
+    def test_revoke_access_token(self):
+        self.check_service_client_function(
+            self.client.revoke_access_token,
+            'tempest.lib.common.rest_client.RestClient.delete',
+            {},
+            user_id=self.FAKE_ACCESS_TOKEN_INFO['access_token']['consumer_id'],
+            access_token_id=self.FAKE_ACCESS_TOKEN_INFO['access_token']['id'],
+            status=204)
+
+    def test_list_access_token_roles(self):
+        self.check_service_client_function(
+            self.client.list_access_token_roles,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_LIST_ACCESS_TOKEN_ROLES,
+            user_id='ce9e07',
+            access_token_id=self.FAKE_ACCESS_TOKEN_INFO['access_token']['id'],
+            status=200)
+
+    def test_get_access_token_role(self):
+        self.check_service_client_function(
+            self.client.get_access_token_role,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_ACCESS_TOKEN_ROLE_INFO,
+            user_id='ce9e07',
+            access_token_id=self.FAKE_ACCESS_TOKEN_INFO['access_token']['id'],
+            role_id=self.FAKE_ACCESS_TOKEN_ROLE_INFO['role']['id'],
+            status=200)
diff --git a/tempest/tests/lib/services/network/test_base_network_client.py b/tempest/tests/lib/services/network/test_base_network_client.py
new file mode 100644
index 0000000..e121cec
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_base_network_client.py
@@ -0,0 +1,96 @@
+# Copyright 2017 AT&T 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.
+
+import mock
+
+from tempest.lib.services.network import base as base_network_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib import fake_http
+from tempest.tests.lib.services import base
+
+
+class TestBaseNetworkClient(base.BaseServiceTest):
+
+    def setUp(self):
+        super(TestBaseNetworkClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = base_network_client.BaseNetworkClient(
+            fake_auth, 'compute', 'regionOne')
+
+        self.mock_expected_success = mock.patch.object(
+            self.client, 'expected_success').start()
+
+    def _assert_empty(self, resp):
+        self.assertEqual([], list(resp.keys()))
+
+    @mock.patch('tempest.lib.common.rest_client.RestClient.post')
+    def test_create_resource(self, mock_post):
+        response = fake_http.fake_http_response(headers=None, status=201)
+        mock_post.return_value = response, '{"baz": "qux"}'
+
+        post_data = {'foo': 'bar'}
+        resp = self.client.create_resource('/fake_url', post_data)
+
+        self.assertEqual({'status': '201'}, resp.response)
+        self.assertEqual("qux", resp["baz"])
+        mock_post.assert_called_once_with('v2.0/fake_url', '{"foo": "bar"}')
+        self.mock_expected_success.assert_called_once_with(
+            201, 201)
+
+    @mock.patch('tempest.lib.common.rest_client.RestClient.post')
+    def test_create_resource_expect_different_values(self, mock_post):
+        response = fake_http.fake_http_response(headers=None, status=200)
+        mock_post.return_value = response, '{}'
+
+        post_data = {'foo': 'bar'}
+        resp = self.client.create_resource('/fake_url', post_data,
+                                           expect_response_code=200,
+                                           expect_empty_body=True)
+
+        self.assertEqual({'status': '200'}, resp.response)
+        self._assert_empty(resp)
+        mock_post.assert_called_once_with('v2.0/fake_url', '{"foo": "bar"}')
+        self.mock_expected_success.assert_called_once_with(
+            200, 200)
+
+    @mock.patch('tempest.lib.common.rest_client.RestClient.put')
+    def test_update_resource(self, mock_put):
+        response = fake_http.fake_http_response(headers=None, status=200)
+        mock_put.return_value = response, '{"baz": "qux"}'
+
+        put_data = {'foo': 'bar'}
+        resp = self.client.update_resource('/fake_url', put_data)
+
+        self.assertEqual({'status': '200'}, resp.response)
+        self.assertEqual("qux", resp["baz"])
+        mock_put.assert_called_once_with('v2.0/fake_url', '{"foo": "bar"}')
+        self.mock_expected_success.assert_called_once_with(
+            200, 200)
+
+    @mock.patch('tempest.lib.common.rest_client.RestClient.put')
+    def test_update_resource_expect_different_values(self, mock_put):
+        response = fake_http.fake_http_response(headers=None, status=201)
+        mock_put.return_value = response, '{}'
+
+        put_data = {'foo': 'bar'}
+        resp = self.client.update_resource('/fake_url', put_data,
+                                           expect_response_code=201,
+                                           expect_empty_body=True)
+
+        self.assertEqual({'status': '201'}, resp.response)
+        self._assert_empty(resp)
+        mock_put.assert_called_once_with('v2.0/fake_url', '{"foo": "bar"}')
+        self.mock_expected_success.assert_called_once_with(
+            201, 201)
diff --git a/tempest/tests/lib/services/network/test_quotas_client.py b/tempest/tests/lib/services/network/test_quotas_client.py
new file mode 100644
index 0000000..e76bc9c
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_quotas_client.py
@@ -0,0 +1,99 @@
+# Copyright 2017 AT&T 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.
+
+from tempest.lib.services.network import quotas_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestQuotasClient(base.BaseServiceTest):
+
+    FAKE_QUOTAS = {
+        "quotas": [
+            {
+                "floatingip": 50,
+                "network": 15,
+                "port": 50,
+                "project_id": "bab7d5c60cd041a0a36f7c4b6e1dd978",
+                "rbac_policy": -1,
+                "router": 10,
+                "security_group": 10,
+                "security_group_rule": 100,
+                "subnet": 10,
+                "subnetpool": -1,
+                "tenant_id": "bab7d5c60cd041a0a36f7c4b6e1dd978"
+            }
+        ]
+    }
+
+    FAKE_QUOTA_TENANT_ID = "bab7d5c60cd041a0a36f7c4b6e1dd978"
+
+    def setUp(self):
+        super(TestQuotasClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.quotas_client = quotas_client.QuotasClient(
+            fake_auth, "network", "regionOne")
+
+    def _test_list_quotas(self, bytes_body=False):
+        self.check_service_client_function(
+            self.quotas_client.list_quotas,
+            "tempest.lib.common.rest_client.RestClient.get",
+            self.FAKE_QUOTAS,
+            bytes_body,
+            200)
+
+    def _test_show_quotas(self, bytes_body=False):
+        self.check_service_client_function(
+            self.quotas_client.show_quotas,
+            "tempest.lib.common.rest_client.RestClient.get",
+            {"quota": self.FAKE_QUOTAS["quotas"][0]},
+            bytes_body,
+            200,
+            tenant_id=self.FAKE_QUOTA_TENANT_ID)
+
+    def _test_update_quotas(self, bytes_body=False):
+        self.check_service_client_function(
+            self.quotas_client.update_quotas,
+            "tempest.lib.common.rest_client.RestClient.put",
+            {"quota": self.FAKE_QUOTAS["quotas"][0]},
+            bytes_body,
+            200,
+            tenant_id=self.FAKE_QUOTA_TENANT_ID)
+
+    def test_reset_quotas(self):
+        self.check_service_client_function(
+            self.quotas_client.reset_quotas,
+            "tempest.lib.common.rest_client.RestClient.delete",
+            {},
+            status=204,
+            tenant_id=self.FAKE_QUOTA_TENANT_ID)
+
+    def test_list_quotas_with_str_body(self):
+        self._test_list_quotas()
+
+    def test_list_quotas_with_bytes_body(self):
+        self._test_list_quotas(bytes_body=True)
+
+    def test_show_quotas_with_str_body(self):
+        self._test_show_quotas()
+
+    def test_show_quotas_with_bytes_body(self):
+        self._test_show_quotas(bytes_body=True)
+
+    def test_update_quotas_with_str_body(self):
+        self._test_update_quotas()
+
+    def test_update_quotas_with_bytes_body(self):
+        self._test_update_quotas(bytes_body=True)
diff --git a/tempest/tests/lib/services/network/test_security_group_rules_client.py b/tempest/tests/lib/services/network/test_security_group_rules_client.py
index ebffcbe..b9c17a1 100644
--- a/tempest/tests/lib/services/network/test_security_group_rules_client.py
+++ b/tempest/tests/lib/services/network/test_security_group_rules_client.py
@@ -15,8 +15,10 @@
 
 import copy
 
+import mock
 from oslo_serialization import jsonutils as json
 
+from tempest.lib.services.network import base as network_base
 from tempest.lib.services.network import security_group_rules_client
 from tempest.tests.lib import fake_auth_provider
 from tempest.tests.lib.services import base
@@ -80,16 +82,22 @@
         kwargs = {'direction': 'egress',
                   'security_group_id': '85cc3048-abc3-43cc-89b3-377341426ac5',
                   'remote_ip_prefix': None}
+        payload = json.dumps({"security_group_rule": kwargs}, sort_keys=True)
+        json_dumps = json.dumps
 
-        self.check_service_client_function(
-            self.client.create_security_group_rule,
-            'tempest.lib.common.rest_client.RestClient.post',
-            self.FAKE_SECURITY_GROUP_RULE,
-            bytes_body,
-            status=201,
-            mock_args=['v2.0/security-group-rules',
-                       json.dumps({"security_group_rule": kwargs})],
-            **kwargs)
+        # NOTE: Use sort_keys for json.dumps so that the expected and actual
+        # payloads are guaranteed to be identical for mock_args assert check.
+        with mock.patch.object(network_base.json, 'dumps') as mock_dumps:
+            mock_dumps.side_effect = lambda d: json_dumps(d, sort_keys=True)
+
+            self.check_service_client_function(
+                self.client.create_security_group_rule,
+                'tempest.lib.common.rest_client.RestClient.post',
+                self.FAKE_SECURITY_GROUP_RULE,
+                bytes_body,
+                status=201,
+                mock_args=['v2.0/security-group-rules', payload],
+                **kwargs)
 
     def _test_show_security_group_rule(self, bytes_body=False):
         self.check_service_client_function(
diff --git a/tempest/tests/lib/services/network/test_security_groups_client.py b/tempest/tests/lib/services/network/test_security_groups_client.py
index d066378..f96805f 100644
--- a/tempest/tests/lib/services/network/test_security_groups_client.py
+++ b/tempest/tests/lib/services/network/test_security_groups_client.py
@@ -15,8 +15,10 @@
 
 import copy
 
+import mock
 from oslo_serialization import jsonutils as json
 
+from tempest.lib.services.network import base as network_base
 from tempest.lib.services.network import security_groups_client
 from tempest.tests.lib import fake_auth_provider
 from tempest.tests.lib.services import base
@@ -89,15 +91,22 @@
 
     def _test_create_security_group(self, bytes_body=False):
         kwargs = {'name': 'fake-security-group-name'}
-        self.check_service_client_function(
-            self.client.create_security_group,
-            'tempest.lib.common.rest_client.RestClient.post',
-            self.FAKE_SECURITY_GROUP,
-            bytes_body,
-            status=201,
-            mock_args=['v2.0/security-groups',
-                       json.dumps({"security_group": kwargs})],
-            **kwargs)
+        payload = json.dumps({"security_group": kwargs}, sort_keys=True)
+        json_dumps = json.dumps
+
+        # NOTE: Use sort_keys for json.dumps so that the expected and actual
+        # payloads are guaranteed to be identical for mock_args assert check.
+        with mock.patch.object(network_base.json, 'dumps') as mock_dumps:
+            mock_dumps.side_effect = lambda d: json_dumps(d, sort_keys=True)
+
+            self.check_service_client_function(
+                self.client.create_security_group,
+                'tempest.lib.common.rest_client.RestClient.post',
+                self.FAKE_SECURITY_GROUP,
+                bytes_body,
+                status=201,
+                mock_args=['v2.0/security-groups', payload],
+                **kwargs)
 
     def _test_show_security_group(self, bytes_body=False):
         self.check_service_client_function(
@@ -113,15 +122,23 @@
         resp_body = copy.deepcopy(self.FAKE_SECURITY_GROUP)
         resp_body["security_group"]["name"] = 'updated-security-group-name'
 
-        self.check_service_client_function(
-            self.client.update_security_group,
-            'tempest.lib.common.rest_client.RestClient.put',
-            resp_body,
-            bytes_body,
-            security_group_id=self.FAKE_SEC_GROUP_ID,
-            mock_args=['v2.0/security-groups/%s' % self.FAKE_SEC_GROUP_ID,
-                       json.dumps({'security_group': kwargs})],
-            **kwargs)
+        payload = json.dumps({'security_group': kwargs}, sort_keys=True)
+        json_dumps = json.dumps
+
+        # NOTE: Use sort_keys for json.dumps so that the expected and actual
+        # payloads are guaranteed to be identical for mock_args assert check.
+        with mock.patch.object(network_base.json, 'dumps') as mock_dumps:
+            mock_dumps.side_effect = lambda d: json_dumps(d, sort_keys=True)
+
+            self.check_service_client_function(
+                self.client.update_security_group,
+                'tempest.lib.common.rest_client.RestClient.put',
+                resp_body,
+                bytes_body,
+                security_group_id=self.FAKE_SEC_GROUP_ID,
+                mock_args=['v2.0/security-groups/%s' % self.FAKE_SEC_GROUP_ID,
+                           payload],
+                **kwargs)
 
     def test_list_security_groups_with_str_body(self):
         self._test_list_security_groups()
diff --git a/tempest/tests/lib/services/volume/v2/test_scheduler_stats_client.py b/tempest/tests/lib/services/volume/v2/test_scheduler_stats_client.py
new file mode 100644
index 0000000..8a5f25f
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v2/test_scheduler_stats_client.py
@@ -0,0 +1,83 @@
+# Copyright 2017 AT&T 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.
+
+from tempest.lib.services.volume.v2 import scheduler_stats_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestSchedulerStatsClient(base.BaseServiceTest):
+    FAKE_POOLS_LIST = {
+        "pools": [
+            {
+                "name": "pool1",
+                "capabilities": {
+                    "updated": "2014-10-28T00:00:00-00:00",
+                    "total_capacity": 1024,
+                    "free_capacity": 100,
+                    "volume_backend_name": "pool1",
+                    "reserved_percentage": 0,
+                    "driver_version": "1.0.0",
+                    "storage_protocol": "iSCSI",
+                    "QoS_support": False
+                }
+            },
+            {
+                "name": "pool2",
+                "capabilities": {
+                    "updated": "2014-10-28T00:00:00-00:00",
+                    "total_capacity": 512,
+                    "free_capacity": 200,
+                    "volume_backend_name": "pool2",
+                    "reserved_percentage": 0,
+                    "driver_version": "1.0.2",
+                    "storage_protocol": "iSER",
+                    "QoS_support": True
+                }
+            }
+        ]
+    }
+
+    def setUp(self):
+        super(TestSchedulerStatsClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = scheduler_stats_client.SchedulerStatsClient(
+            fake_auth, 'volume', 'regionOne')
+
+    def _test_list_pools(self, bytes_body=False, detail=False):
+        resp_body = []
+        if detail:
+            resp_body = self.FAKE_POOLS_LIST
+        else:
+            resp_body = {'pools': [{'name': pool['name']}
+                         for pool in self.FAKE_POOLS_LIST['pools']]}
+        self.check_service_client_function(
+            self.client.list_pools,
+            'tempest.lib.common.rest_client.RestClient.get',
+            resp_body,
+            bytes_body,
+            detail=detail)
+
+    def test_list_pools_with_str_body(self):
+        self._test_list_pools()
+
+    def test_list_pools_with_str_body_and_detail(self):
+        self._test_list_pools(detail=True)
+
+    def test_list_pools_with_bytes_body(self):
+        self._test_list_pools(bytes_body=True)
+
+    def test_list_pools_with_bytes_body_and_detail(self):
+        self._test_list_pools(bytes_body=True, detail=True)
diff --git a/tempest/tests/lib/services/volume/v3/test_group_types_client.py b/tempest/tests/lib/services/volume/v3/test_group_types_client.py
new file mode 100644
index 0000000..95498c7
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v3/test_group_types_client.py
@@ -0,0 +1,57 @@
+# Copyright (C) 2017 Dell Inc. or its subsidiaries.
+#
+# 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 group_types_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestGroupTypesClient(base.BaseServiceTest):
+    FAKE_CREATE_GROUP_TYPE = {
+        "group_type": {
+            "name": "group-type-001",
+            "description": "Test group type 1",
+            "group_specs": {},
+            "is_public": True,
+        }
+    }
+
+    def setUp(self):
+        super(TestGroupTypesClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = group_types_client.GroupTypesClient(fake_auth,
+                                                          'volume',
+                                                          'regionOne')
+
+    def _test_create_group_type(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.create_group_type,
+            'tempest.lib.common.rest_client.RestClient.post',
+            self.FAKE_CREATE_GROUP_TYPE,
+            bytes_body,
+            status=202)
+
+    def test_create_group_type_with_str_body(self):
+        self._test_create_group_type()
+
+    def test_create_group_type_with_bytes_body(self):
+        self._test_create_group_type(bytes_body=True)
+
+    def test_delete_group_type(self):
+        self.check_service_client_function(
+            self.client.delete_group_type,
+            'tempest.lib.common.rest_client.RestClient.delete',
+            {},
+            group_type_id='0e58433f-d108-4bf3-a22c-34e6b71ef86b',
+            status=202)
diff --git a/tempest/tests/lib/services/volume/v3/test_groups_client.py b/tempest/tests/lib/services/volume/v3/test_groups_client.py
new file mode 100644
index 0000000..00db5b4
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v3/test_groups_client.py
@@ -0,0 +1,136 @@
+# Copyright (C) 2017 Dell Inc. or its subsidiaries.
+#
+# 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 groups_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestGroupsClient(base.BaseServiceTest):
+    FAKE_CREATE_GROUP = {
+        "group": {
+            "name": "group-001",
+            "description": "Test group 1",
+            "group_type": "0e58433f-d108-4bf3-a22c-34e6b71ef86b",
+            "volume_types": ["2103099d-7cc3-4e52-a2f1-23a5284416f3"],
+            "availability_zone": "az1",
+        }
+    }
+
+    FAKE_INFO_GROUP = {
+        "group": {
+            "id": "0e701ab8-1bec-4b9f-b026-a7ba4af13578",
+            "name": "group-001",
+            "description": "Test group 1",
+            "group_type": "0e58433f-d108-4bf3-a22c-34e6b71ef86b",
+            "volume_types": ["2103099d-7cc3-4e52-a2f1-23a5284416f3"],
+            "status": "available",
+            "availability_zone": "az1",
+            "created_at": "20127-06-20T03:50:07Z"
+        }
+    }
+
+    FAKE_LIST_GROUPS = {
+        "groups": [
+            {
+                "id": "0e701ab8-1bec-4b9f-b026-a7ba4af13578",
+                "name": "group-001",
+                "description": "Test group 1",
+                "group_type": "0e58433f-d108-4bf3-a22c-34e6b71ef86b",
+                "volume_types": ["2103099d-7cc3-4e52-a2f1-23a5284416f3"],
+                "status": "available",
+                "availability_zone": "az1",
+                "created_at": "2017-06-20T03:50:07Z",
+            },
+            {
+                "id": "e479997c-650b-40a4-9dfe-77655818b0d2",
+                "name": "group-002",
+                "description": "Test group 2",
+                "group_snapshot_id": "79c9afdb-7e46-4d71-9249-1f022886963c",
+                "group_type": "0e58433f-d108-4bf3-a22c-34e6b71ef86b",
+                "volume_types": ["2103099d-7cc3-4e52-a2f1-23a5284416f3"],
+                "status": "available",
+                "availability_zone": "az1",
+                "created_at": "2017-06-19T01:52:47Z",
+            },
+            {
+                "id": "c5c4769e-213c-40a6-a568-8e797bb691d4",
+                "name": "group-003",
+                "description": "Test group 3",
+                "source_group_id": "e92f9dc7-0b20-492d-8ab2-3ad8fdac270e",
+                "group_type": "0e58433f-d108-4bf3-a22c-34e6b71ef86b",
+                "volume_types": ["2103099d-7cc3-4e52-a2f1-23a5284416f3"],
+                "status": "available",
+                "availability_zone": "az1",
+                "created_at": "2017-06-18T06:34:32Z",
+            }
+        ]
+    }
+
+    def setUp(self):
+        super(TestGroupsClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = groups_client.GroupsClient(fake_auth,
+                                                 'volume',
+                                                 'regionOne')
+
+    def _test_create_group(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.create_group,
+            'tempest.lib.common.rest_client.RestClient.post',
+            self.FAKE_CREATE_GROUP,
+            bytes_body,
+            status=202)
+
+    def _test_show_group(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.show_group,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_INFO_GROUP,
+            bytes_body,
+            group_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5")
+
+    def _test_list_groups(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_groups,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_LIST_GROUPS,
+            bytes_body,
+            detail=True)
+
+    def test_create_group_with_str_body(self):
+        self._test_create_group()
+
+    def test_create_group_with_bytes_body(self):
+        self._test_create_group(bytes_body=True)
+
+    def test_show_group_with_str_body(self):
+        self._test_show_group()
+
+    def test_show_group_with_bytes_body(self):
+        self._test_show_group(bytes_body=True)
+
+    def test_list_groups_with_str_body(self):
+        self._test_list_groups()
+
+    def test_list_groups_with_bytes_body(self):
+        self._test_list_groups(bytes_body=True)
+
+    def test_delete_group(self):
+        self.check_service_client_function(
+            self.client.delete_group,
+            'tempest.lib.common.rest_client.RestClient.post',
+            {},
+            group_id='0e701ab8-1bec-4b9f-b026-a7ba4af13578',
+            status=202)
diff --git a/test-requirements.txt b/test-requirements.txt
index 19b45ea..04fd878 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -3,11 +3,10 @@
 # process, which may cause wedges in the gate later.
 hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
 # needed for doc build
-sphinx!=1.6.1,>=1.5.1 # BSD
-oslosphinx>=4.7.0 # Apache-2.0
+sphinx>=1.6.2 # BSD
+openstackdocstheme>=1.11.0  # Apache-2.0
 reno!=2.3.1,>=1.8.0 # Apache-2.0
 mock>=2.0 # BSD
 coverage!=4.4,>=4.0 # Apache-2.0
 oslotest>=1.10.0 # Apache-2.0
 flake8-import-order==0.11 # LGPLv3
-openstackdocstheme>=1.5.0 # Apache-2.0
diff --git a/tools/generate-tempest-plugins-list.py b/tools/generate-tempest-plugins-list.py
index a33962b..99df0d1 100644
--- a/tools/generate-tempest-plugins-list.py
+++ b/tools/generate-tempest-plugins-list.py
@@ -28,10 +28,13 @@
 
 try:
     # For Python 3.0 and later
-    import urllib
+    from urllib.error import HTTPError as HTTPError
+    import urllib.request as urllib
 except ImportError:
     # Fall back to Python 2's urllib2
     import urllib2 as urllib
+    from urllib2 import HTTPError as HTTPError
+
 
 url = 'https://review.openstack.org/projects/'
 
@@ -55,13 +58,13 @@
 
 def has_tempest_plugin(proj):
     try:
-        r = urllib.urlopen("https://git.openstack.org/cgit/%s/plain/setup.cfg"
-                           % proj)
-    except urllib.HTTPError as err:
+        r = urllib.urlopen(
+            "https://git.openstack.org/cgit/%s/plain/setup.cfg" % proj)
+    except HTTPError as err:
         if err.code == 404:
             return False
     p = re.compile('^tempest\.test_plugins', re.M)
-    if p.findall(r.read()):
+    if p.findall(r.read().decode('utf-8')):
         return True
     else:
         False