Merge "Add python3-dev(el) to bindep.txt"
diff --git a/releasenotes/notes/add-compute-feature-serial-console-45583c4341e34fc9.yaml b/releasenotes/notes/add-compute-feature-serial-console-45583c4341e34fc9.yaml
new file mode 100644
index 0000000..18fd5ad
--- /dev/null
+++ b/releasenotes/notes/add-compute-feature-serial-console-45583c4341e34fc9.yaml
@@ -0,0 +1,7 @@
+---
+features:
+  - |
+    A new boolean config option ``serial_console`` is added to the section
+    ``compute-feature-enabled``. If enabled, tests, which validate the
+    behavior of Nova's *serial console* feature (an alternative to VNC,
+    RDP, SPICE) can be executed.
diff --git a/releasenotes/notes/add-force-detach-volume-to-volumes-client-library-b2071f2954f8e8b1.yaml b/releasenotes/notes/add-force-detach-volume-to-volumes-client-library-b2071f2954f8e8b1.yaml
new file mode 100644
index 0000000..a0156a0
--- /dev/null
+++ b/releasenotes/notes/add-force-detach-volume-to-volumes-client-library-b2071f2954f8e8b1.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    Add force detach volume feature API to v2 volumes_client library.
+    This feature enables the possibility to force a volume to detach, and
+    roll back an unsuccessful detach operation after you disconnect the volume.
diff --git a/releasenotes/notes/add-identity-v3-clients-for-os-ep-filter-api-extensions-9cfd217fd2c6a61f.yaml b/releasenotes/notes/add-identity-v3-clients-for-os-ep-filter-api-extensions-9cfd217fd2c6a61f.yaml
new file mode 100644
index 0000000..69320fb
--- /dev/null
+++ b/releasenotes/notes/add-identity-v3-clients-for-os-ep-filter-api-extensions-9cfd217fd2c6a61f.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    Defines the identity v3 OS-EP-FILTER extension API client.
+    This client manages associations between endpoints, projects
+    along with groups.
diff --git a/releasenotes/notes/deprecate-compute-images-client-in-volume-tests-92b6dd55fcaba620.yaml b/releasenotes/notes/deprecate-compute-images-client-in-volume-tests-92b6dd55fcaba620.yaml
new file mode 100644
index 0000000..dc4ed27
--- /dev/null
+++ b/releasenotes/notes/deprecate-compute-images-client-in-volume-tests-92b6dd55fcaba620.yaml
@@ -0,0 +1,10 @@
+---
+deprecations:
+  - |
+    Image APIs in compute are deprecated, Image native APIs are recommended.
+    And Glance v1 APIs are deprecated and v2 APIs are current. Image client
+    compute_images_client and Glance v1 APIs are removed in volume tests.
+upgrade:
+  - |
+    Swith to use Glance v2 APIs in volume tests, by adding the Glance v2 client
+    images_client.
diff --git a/setup.cfg b/setup.cfg
index b2035bc..b292970 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -39,7 +39,11 @@
     cleanup = tempest.cmd.cleanup:TempestCleanup
     list-plugins = tempest.cmd.list_plugins:TempestListPlugins
     verify-config = tempest.cmd.verify_tempest_config:TempestVerifyConfig
-    workspace = tempest.cmd.workspace:TempestWorkspace
+    workspace_register = tempest.cmd.workspace:TempestWorkspaceRegister
+    workspace_rename = tempest.cmd.workspace:TempestWorkspaceRename
+    workspace_move = tempest.cmd.workspace:TempestWorkspaceMove
+    workspace_remove = tempest.cmd.workspace:TempestWorkspaceRemove
+    workspace_list = tempest.cmd.workspace:TempestWorkspaceList
     run = tempest.cmd.run:TempestRun
 oslo.config.opts =
     tempest.config = tempest.config:list_opts
diff --git a/tempest/api/compute/admin/test_live_migration.py b/tempest/api/compute/admin/test_live_migration.py
index 4d0f12a..8344103 100644
--- a/tempest/api/compute/admin/test_live_migration.py
+++ b/tempest/api/compute/admin/test_live_migration.py
@@ -13,10 +13,13 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import time
+
 from oslo_log import log as logging
 import testtools
 
 from tempest.api.compute import base
+from tempest.common import compute
 from tempest.common import waiters
 from tempest import config
 from tempest.lib import decorators
@@ -175,6 +178,80 @@
         self.assertEqual(volume_id1, volume_id2)
 
 
+class LiveBlockMigrationRemoteConsolesV26TestJson(LiveBlockMigrationTestJSON):
+    min_microversion = '2.6'
+    max_microversion = 'latest'
+
+    @decorators.idempotent_id('6190af80-513e-4f0f-90f2-9714e84955d7')
+    @testtools.skipUnless(CONF.compute_feature_enabled.serial_console,
+                          'Serial console not supported.')
+    @testtools.skipUnless(
+        test.is_scheduler_filter_enabled("DifferentHostFilter"),
+        'DifferentHostFilter is not available.')
+    def test_live_migration_serial_console(self):
+        """Test the live-migration of an instance which has a serial console
+
+        The serial console feature of an instance uses ports on the host.
+        These ports need to be updated when they are already in use by
+        another instance on the target host. This test checks if this
+        update behavior is correctly done, by connecting to the serial
+        consoles of the instances before and after the live migration.
+        """
+        server01_id = self.create_test_server(wait_until='ACTIVE')['id']
+        hints = {'different_host': server01_id}
+        server02_id = self.create_test_server(scheduler_hints=hints,
+                                              wait_until='ACTIVE')['id']
+        host01_id = self._get_host_for_server(server01_id)
+        host02_id = self._get_host_for_server(server02_id)
+        self.assertNotEqual(host01_id, host02_id)
+
+        # At this step we have 2 instances on different hosts, both with
+        # serial consoles, both with port 10000 (the default value).
+        # https://bugs.launchpad.net/nova/+bug/1455252 describes the issue
+        # when live-migrating in such a scenario.
+
+        self._verify_console_interaction(server01_id)
+        self._verify_console_interaction(server02_id)
+
+        self._migrate_server_to(server01_id, host02_id)
+        waiters.wait_for_server_status(self.servers_client,
+                                       server01_id, 'ACTIVE')
+        self.assertEqual(host02_id, self._get_host_for_server(server01_id))
+        self._verify_console_interaction(server01_id)
+        # At this point, both instances have a valid serial console
+        # connection, which means the ports got updated.
+
+    def _verify_console_interaction(self, server_id):
+        body = self.servers_client.get_remote_console(server_id,
+                                                      console_type='serial',
+                                                      protocol='serial')
+        console_url = body['remote_console']['url']
+        data = "test_live_migration_serial_console"
+        console_output = ''
+        t = 0.0
+        interval = 0.1
+
+        ws = compute.create_websocket(console_url)
+        try:
+            # NOTE (markus_z): It can take a long time until the terminal
+            # of the instance is available for interaction. Hence the
+            # long timeout value.
+            while data not in console_output and t <= 120.0:
+                try:
+                    ws.send_frame(data)
+                    recieved = ws.receive_frame()
+                    console_output += recieved
+                except Exception:
+                    # In case we had an issue with send/receive on the
+                    # websocket connection, we create a new one.
+                    ws = compute.create_websocket(console_url)
+                time.sleep(interval)
+                t += interval
+        finally:
+            ws.close()
+        self.assertIn(data, console_output)
+
+
 class LiveAutoBlockMigrationV225TestJSON(LiveBlockMigrationTestJSON):
     min_microversion = '2.25'
     max_microversion = 'latest'
diff --git a/tempest/api/compute/flavors/test_flavors_negative.py b/tempest/api/compute/flavors/test_flavors_negative.py
index b2c33e2..91e9684 100644
--- a/tempest/api/compute/flavors/test_flavors_negative.py
+++ b/tempest/api/compute/flavors/test_flavors_negative.py
@@ -82,9 +82,9 @@
         self.assertEqual(min_img_ram, image['min_ram'])
 
         # Try to create server with flavor of insufficient ram size
-        self.assertRaisesRegexp(lib_exc.BadRequest,
-                                "Flavor's memory is too small for "
-                                "requested image",
-                                self.create_test_server,
-                                image_id=image['id'],
-                                flavor=flavor['id'])
+        self.assertRaisesRegex(lib_exc.BadRequest,
+                               "Flavor's memory is too small for "
+                               "requested image",
+                               self.create_test_server,
+                               image_id=image['id'],
+                               flavor=flavor['id'])
diff --git a/tempest/api/identity/admin/v3/test_domains.py b/tempest/api/identity/admin/v3/test_domains.py
index 86a5764..9683e93 100644
--- a/tempest/api/identity/admin/v3/test_domains.py
+++ b/tempest/api/identity/admin/v3/test_domains.py
@@ -31,10 +31,7 @@
         # One of those domains will be disabled
         cls.setup_domains = list()
         for i in range(3):
-            domain = cls.domains_client.create_domain(
-                name=data_utils.rand_name('domain'),
-                description=data_utils.rand_name('domain-desc'),
-                enabled=i < 2)['domain']
+            domain = cls.create_domain(enabled=i < 2)
             cls.setup_domains.append(domain)
 
     @classmethod
diff --git a/tempest/api/identity/admin/v3/test_domains_negative.py b/tempest/api/identity/admin/v3/test_domains_negative.py
index 61b4fa2..1a0b851 100644
--- a/tempest/api/identity/admin/v3/test_domains_negative.py
+++ b/tempest/api/identity/admin/v3/test_domains_negative.py
@@ -25,11 +25,7 @@
     @decorators.attr(type=['negative', 'gate'])
     @decorators.idempotent_id('1f3fbff5-4e44-400d-9ca1-d953f05f609b')
     def test_delete_active_domain(self):
-        d_name = data_utils.rand_name('domain')
-        d_desc = data_utils.rand_name('domain-desc')
-        domain = self.domains_client.create_domain(
-            name=d_name,
-            description=d_desc)['domain']
+        domain = self.create_domain()
         domain_id = domain['id']
 
         self.addCleanup(self.delete_domain, domain_id)
diff --git a/tempest/api/identity/admin/v3/test_inherits.py b/tempest/api/identity/admin/v3/test_inherits.py
index f630f74..49b6585 100644
--- a/tempest/api/identity/admin/v3/test_inherits.py
+++ b/tempest/api/identity/admin/v3/test_inherits.py
@@ -31,9 +31,7 @@
         u_desc = '%s description' % u_name
         u_email = '%s@testmail.tm' % u_name
         u_password = data_utils.rand_name('pass-')
-        cls.domain = cls.domains_client.create_domain(
-            name=data_utils.rand_name('domain-'),
-            description=data_utils.rand_name('domain-desc-'))['domain']
+        cls.domain = cls.create_domain()
         cls.project = cls.projects_client.create_project(
             data_utils.rand_name('project-'),
             description=data_utils.rand_name('project-desc-'),
diff --git a/tempest/api/identity/admin/v3/test_roles.py b/tempest/api/identity/admin/v3/test_roles.py
index ac56fc6..adb467c 100644
--- a/tempest/api/identity/admin/v3/test_roles.py
+++ b/tempest/api/identity/admin/v3/test_roles.py
@@ -38,9 +38,7 @@
         u_desc = '%s description' % u_name
         u_email = '%s@testmail.tm' % u_name
         cls.u_password = data_utils.rand_password()
-        cls.domain = cls.domains_client.create_domain(
-            name=data_utils.rand_name('domain'),
-            description=data_utils.rand_name('domain-desc'))['domain']
+        cls.domain = cls.create_domain()
         cls.project = cls.projects_client.create_project(
             data_utils.rand_name('project'),
             description=data_utils.rand_name('project-desc'),
diff --git a/tempest/api/identity/admin/v3/test_trusts.py b/tempest/api/identity/admin/v3/test_trusts.py
index 339b4bb..0a163fc 100644
--- a/tempest/api/identity/admin/v3/test_trusts.py
+++ b/tempest/api/identity/admin/v3/test_trusts.py
@@ -232,10 +232,12 @@
         # For example, when creating a trust, we will set the expiry time of
         # the trust to 2015-02-17T17:34:01.907051Z. However, if we make a GET
         # request on the trust, the response will contain the time rounded up
-        # to 2015-02-17T17:34:02.000000Z. That is why we shouldn't set flag
-        # "subsecond" to True when we invoke timeutils.isotime(...) to avoid
-        # problems with rounding.
-        expires_str = timeutils.isotime(at=expires_at)
+        # to 2015-02-17T17:34:02.000000Z. That is why we set microsecond to
+        # 0 when we invoke isoformat to avoid problems with rounding.
+        expires_at = expires_at.replace(microsecond=0)
+        # NOTE(ekhugen) Python datetime does not support military timezones
+        # since we used UTC we'll add the Z so our compare works.
+        expires_str = expires_at.isoformat() + 'Z'
 
         trust = self.create_trust(expires=expires_str)
         self.validate_trust(trust, expires=expires_str)
diff --git a/tempest/api/identity/base.py b/tempest/api/identity/base.py
index 5dc88b8..10121d9 100644
--- a/tempest/api/identity/base.py
+++ b/tempest/api/identity/base.py
@@ -223,6 +223,7 @@
         cls.role_assignments = cls.os_admin.role_assignments_client
         cls.oauth_consumers_client = cls.os_adm.oauth_consumers_client
         cls.domain_config_client = cls.os_adm.domain_config_client
+        cls.endpoint_filter_client = cls.os_adm.endpoint_filter_client
         if CONF.identity.admin_domain_scope:
             # NOTE(andreaf) When keystone policy requires it, the identity
             # admin clients for these tests shall use 'domain' scoped tokens.
@@ -236,11 +237,13 @@
         cls.users_client.update_user(user['id'], name=user_name, enabled=False)
 
     @classmethod
-    def create_domain(cls):
+    def create_domain(cls, **kwargs):
         """Create a domain."""
-        domain = cls.domains_client.create_domain(
-            name=data_utils.rand_name('test_domain'),
-            description=data_utils.rand_name('desc'))['domain']
+        if 'name' not in kwargs:
+            kwargs['name'] = data_utils.rand_name('test_domain')
+        if 'description' not in kwargs:
+            kwargs['description'] = data_utils.rand_name('desc')
+        domain = cls.domains_client.create_domain(**kwargs)['domain']
         return domain
 
     def delete_domain(self, domain_id):
diff --git a/tempest/api/volume/admin/test_volumes_actions.py b/tempest/api/volume/admin/test_volumes_actions.py
index 7f291e9..acff7cd 100644
--- a/tempest/api/volume/admin/test_volumes_actions.py
+++ b/tempest/api/volume/admin/test_volumes_actions.py
@@ -14,7 +14,12 @@
 #    under the License.
 
 from tempest.api.volume import base
+from tempest.common import waiters
+from tempest import config
 from tempest.lib import decorators
+from tempest import test
+
+CONF = config.CONF
 
 
 class VolumesActionsTest(base.BaseVolumeAdminTest):
@@ -60,3 +65,36 @@
     def test_volume_force_delete_when_volume_is_maintenance(self):
         # test force delete when status of volume is maintenance
         self._create_reset_and_force_delete_temp_volume('maintenance')
+
+    @decorators.idempotent_id('d38285d9-929d-478f-96a5-00e66a115b81')
+    @test.services('compute')
+    def test_force_detach_volume(self):
+        # Create a server and a volume
+        server_id = self.create_server(wait_until='ACTIVE')['id']
+        volume_id = self.create_volume()['id']
+
+        # Attach volume
+        self.volumes_client.attach_volume(
+            volume_id,
+            instance_uuid=server_id,
+            mountpoint='/dev/%s' % CONF.compute.volume_device_name)
+        waiters.wait_for_volume_resource_status(self.volumes_client,
+                                                volume_id, 'in-use')
+        self.addCleanup(waiters.wait_for_volume_resource_status,
+                        self.volumes_client, volume_id, 'available')
+        self.addCleanup(self.volumes_client.detach_volume, volume_id)
+        attachment = self.volumes_client.show_volume(
+            volume_id)['volume']['attachments'][0]
+
+        # Reset volume's status to error
+        self.admin_volume_client.reset_volume_status(volume_id, status='error')
+
+        # Force detach volume
+        self.admin_volume_client.force_detach_volume(
+            volume_id, connector=None,
+            attachment_id=attachment['attachment_id'])
+        waiters.wait_for_volume_resource_status(self.volumes_client,
+                                                volume_id, 'available')
+        vol_info = self.volumes_client.show_volume(volume_id)['volume']
+        self.assertIn('attachments', vol_info)
+        self.assertEmpty(vol_info['attachments'])
diff --git a/tempest/api/volume/base.py b/tempest/api/volume/base.py
index 52ec423..d8b503d 100644
--- a/tempest/api/volume/base.py
+++ b/tempest/api/volume/base.py
@@ -65,7 +65,6 @@
     def setup_clients(cls):
         super(BaseVolumeTest, cls).setup_clients()
         cls.servers_client = cls.os.servers_client
-        cls.compute_images_client = cls.os.compute_images_client
 
         if CONF.service_available.glance:
             cls.images_client = cls.os.image_client_v2
@@ -116,9 +115,8 @@
             kwargs['size'] = CONF.volume.volume_size
 
         if 'imageRef' in kwargs:
-            image = cls.compute_images_client.show_image(
-                kwargs['imageRef'])['image']
-            min_disk = image.get('minDisk')
+            image = cls.images_client.show_image(kwargs['imageRef'])
+            min_disk = image['min_disk']
             kwargs['size'] = max(kwargs['size'], min_disk)
 
         if 'name' not in kwargs:
diff --git a/tempest/api/volume/test_volumes_actions.py b/tempest/api/volume/test_volumes_actions.py
index 1e05f22..2ed2a06 100644
--- a/tempest/api/volume/test_volumes_actions.py
+++ b/tempest/api/volume/test_volumes_actions.py
@@ -19,7 +19,6 @@
 from tempest.lib.common.utils import data_utils
 from tempest.lib.common.utils import test_utils
 from tempest.lib import decorators
-from tempest.lib import exceptions
 from tempest import test
 
 CONF = config.CONF
@@ -28,20 +27,6 @@
 class VolumesActionsTest(base.BaseVolumeTest):
 
     @classmethod
-    def setup_clients(cls):
-        super(VolumesActionsTest, cls).setup_clients()
-        if CONF.service_available.glance:
-            # Check if glance v1 is available to determine which client to use.
-            if CONF.image_feature_enabled.api_v1:
-                cls.image_client = cls.os.image_client
-            elif CONF.image_feature_enabled.api_v2:
-                cls.image_client = cls.os.image_client_v2
-            else:
-                raise exceptions.InvalidConfiguration(
-                    'Either api_v1 or api_v2 must be True in '
-                    '[image-feature-enabled].')
-
-    @classmethod
     def resource_setup(cls):
         super(VolumesActionsTest, cls).resource_setup()
 
@@ -121,9 +106,9 @@
             disk_format=CONF.volume.disk_format)['os-volume_upload_image']
         image_id = body["image_id"]
         self.addCleanup(test_utils.call_and_ignore_notfound_exc,
-                        self.image_client.delete_image,
+                        self.images_client.delete_image,
                         image_id)
-        waiters.wait_for_image_status(self.image_client, image_id, 'active')
+        waiters.wait_for_image_status(self.images_client, image_id, 'active')
         waiters.wait_for_volume_resource_status(self.volumes_client,
                                                 self.volume['id'], 'available')
 
diff --git a/tempest/api/volume/test_volumes_get.py b/tempest/api/volume/test_volumes_get.py
index 1d9b846..712254e 100644
--- a/tempest/api/volume/test_volumes_get.py
+++ b/tempest/api/volume/test_volumes_get.py
@@ -124,9 +124,8 @@
     @decorators.idempotent_id('54a01030-c7fc-447c-86ee-c1182beae638')
     @test.services('image')
     def test_volume_create_get_update_delete_from_image(self):
-        image = self.compute_images_client.show_image(
-            CONF.compute.image_ref)['image']
-        min_disk = image.get('minDisk')
+        image = self.images_client.show_image(CONF.compute.image_ref)
+        min_disk = image['min_disk']
         disk_size = max(min_disk, CONF.volume.volume_size)
         self._volume_create_get_update_delete(
             imageRef=CONF.compute.image_ref, size=disk_size)
diff --git a/tempest/clients.py b/tempest/clients.py
index 4ef6872..e000a74 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -231,6 +231,8 @@
             **params_v3)
         self.domain_config_client = self.identity_v3.DomainConfigurationClient(
             **params_v3)
+        self.endpoint_filter_client = \
+            self.identity_v3.EndPointsFilterClient(**params_v3)
 
         # Token clients do not use the catalog. They only need default_params.
         # They read auth_url, so they should only be set if the corresponding
diff --git a/tempest/cmd/workspace.py b/tempest/cmd/workspace.py
index d2dc00d..96d2300 100644
--- a/tempest/cmd/workspace.py
+++ b/tempest/cmd/workspace.py
@@ -52,8 +52,8 @@
 import sys
 
 from cliff import command
+from cliff import lister
 from oslo_concurrency import lockutils
-import prettytable
 import yaml
 
 from tempest import config
@@ -154,76 +154,97 @@
             self.workspaces = yaml.safe_load(f) or {}
 
 
-class TempestWorkspace(command.Command):
-    def take_action(self, parsed_args):
-        self.manager = WorkspaceManager(parsed_args.workspace_path)
-        if getattr(parsed_args, 'register', None):
-            self.manager.register_new_workspace(
-                parsed_args.name, parsed_args.path)
-        elif getattr(parsed_args, 'rename', None):
-            self.manager.rename_workspace(
-                parsed_args.old_name, parsed_args.new_name)
-        elif getattr(parsed_args, 'move', None):
-            self.manager.move_workspace(
-                parsed_args.name, parsed_args.path)
-        elif getattr(parsed_args, 'remove', None):
-            self.manager.remove_workspace(
-                parsed_args.name)
-        else:
-            self._print_workspaces()
-        sys.exit(0)
+def add_global_arguments(parser):
+    parser.add_argument(
+        '--workspace-path', required=False, default=None,
+        help="The path to the workspace file, the default is "
+             "~/.tempest/workspace.yaml")
+    return parser
 
+
+class TempestWorkspaceRegister(command.Command):
     def get_description(self):
-        return 'Tempest workspace actions'
+        return ('Registers a new tempest workspace via a given '
+                '--name and --path')
 
     def get_parser(self, prog_name):
-        parser = super(TempestWorkspace, self).get_parser(prog_name)
-
-        parser.add_argument(
-            '--workspace-path', required=False, default=None,
-            help="The path to the workspace file, the default is "
-                 "~/.tempest/workspace.yaml")
-
-        subparsers = parser.add_subparsers()
-
-        list_parser = subparsers.add_parser(
-            'list', help='Outputs the name and path of all known tempest '
-            'workspaces')
-        list_parser.set_defaults(list=True)
-
-        register_parser = subparsers.add_parser(
-            'register', help='Registers a new tempest workspace via a given '
-            '--name and --path')
-        register_parser.add_argument('--name', required=True)
-        register_parser.add_argument('--path', required=True)
-        register_parser.set_defaults(register=True)
-
-        update_parser = subparsers.add_parser(
-            'rename', help='Renames a tempest workspace from --old-name to '
-            '--new-name')
-        update_parser.add_argument('--old-name', required=True)
-        update_parser.add_argument('--new-name', required=True)
-        update_parser.set_defaults(rename=True)
-
-        move_parser = subparsers.add_parser(
-            'move', help='Changes the path of a given tempest workspace '
-            '--name to --path')
-        move_parser.add_argument('--name', required=True)
-        move_parser.add_argument('--path', required=True)
-        move_parser.set_defaults(move=True)
-
-        remove_parser = subparsers.add_parser(
-            'remove', help='Deletes the entry for a given tempest workspace '
-            '--name')
-        remove_parser.add_argument('--name', required=True)
-        remove_parser.set_defaults(remove=True)
+        parser = super(TempestWorkspaceRegister, self).get_parser(prog_name)
+        add_global_arguments(parser)
+        parser.add_argument('--name', required=True)
+        parser.add_argument('--path', required=True)
 
         return parser
 
-    def _print_workspaces(self):
-        output = prettytable.PrettyTable(["Name", "Path"])
-        if self.manager.list_workspaces() is not None:
-            for name, path in self.manager.list_workspaces().items():
-                output.add_row([name, path])
+    def take_action(self, parsed_args):
+        self.manager = WorkspaceManager(parsed_args.workspace_path)
+        self.manager.register_new_workspace(parsed_args.name, parsed_args.path)
+        sys.exit(0)
 
-        print(output)
+
+class TempestWorkspaceRename(command.Command):
+    def get_description(self):
+        return 'Renames a tempest workspace from --old-name to --new-name'
+
+    def get_parser(self, prog_name):
+        parser = super(TempestWorkspaceRename, self).get_parser(prog_name)
+        add_global_arguments(parser)
+        parser.add_argument('--old-name', required=True)
+        parser.add_argument('--new-name', required=True)
+
+        return parser
+
+    def take_action(self, parsed_args):
+        self.manager = WorkspaceManager(parsed_args.workspace_path)
+        self.manager.rename_workspace(
+            parsed_args.old_name, parsed_args.new_name)
+        sys.exit(0)
+
+
+class TempestWorkspaceMove(command.Command):
+    def get_description(self):
+        return 'Changes the path of a given tempest workspace --name to --path'
+
+    def get_parser(self, prog_name):
+        parser = super(TempestWorkspaceMove, self).get_parser(prog_name)
+        add_global_arguments(parser)
+        parser.add_argument('--name', required=True)
+        parser.add_argument('--path', required=True)
+
+        return parser
+
+    def take_action(self, parsed_args):
+        self.manager = WorkspaceManager(parsed_args.workspace_path)
+        self.manager.move_workspace(parsed_args.name, parsed_args.path)
+        sys.exit(0)
+
+
+class TempestWorkspaceRemove(command.Command):
+    def get_description(self):
+        return 'Deletes the entry for a given tempest workspace --name'
+
+    def get_parser(self, prog_name):
+        parser = super(TempestWorkspaceRemove, self).get_parser(prog_name)
+        add_global_arguments(parser)
+        parser.add_argument('--name', required=True)
+
+        return parser
+
+    def take_action(self, parsed_args):
+        self.manager = WorkspaceManager(parsed_args.workspace_path)
+        self.manager.remove_workspace(parsed_args.name)
+        sys.exit(0)
+
+
+class TempestWorkspaceList(lister.Lister):
+    def get_description(self):
+        return 'Outputs the name and path of all known tempest workspaces'
+
+    def get_parser(self, prog_name):
+        parser = super(TempestWorkspaceList, self).get_parser(prog_name)
+        add_global_arguments(parser)
+        return parser
+
+    def take_action(self, parsed_args):
+        self.manager = WorkspaceManager(parsed_args.workspace_path)
+        return (("Name", "Path"),
+                ((n, p) for n, p in self.manager.list_workspaces().items()))
diff --git a/tempest/config.py b/tempest/config.py
index b8a91b7..6a198d7 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -392,9 +392,9 @@
                      "migration"),
     cfg.BoolOpt('block_migrate_cinder_iscsi',
                 default=False,
-                help="Does the test environment block migration support "
-                "cinder iSCSI volumes. Note, libvirt doesn't support this, "
-                "see https://bugs.launchpad.net/nova/+bug/1398999"),
+                help="Does the test environment support block migration with "
+                "Cinder iSCSI volumes. Note: libvirt >= 1.2.17 is required "
+                "to support this if using the libvirt compute driver."),
     cfg.BoolOpt('vnc_console',
                 default=False,
                 help='Enable VNC console. This configuration value should '
@@ -407,6 +407,11 @@
                 default=False,
                 help='Enable RDP console. This configuration value should '
                      'be same as [nova.rdp]->enabled in nova.conf'),
+    cfg.BoolOpt('serial_console',
+                default=False,
+                help='Enable serial console. This configuration value '
+                     'should be the same as [nova.serial_console]->enabled '
+                     'in nova.conf'),
     cfg.BoolOpt('rescue',
                 default=True,
                 help='Does the test environment support instance rescue '
diff --git a/tempest/lib/api_schema/response/compute/v2_6/__init__.py b/tempest/lib/api_schema/response/compute/v2_6/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_6/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_6/servers.py b/tempest/lib/api_schema/response/compute/v2_6/servers.py
new file mode 100644
index 0000000..29b3e86
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_6/servers.py
@@ -0,0 +1,48 @@
+# Copyright 2016 IBM Corp.
+#
+#    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_3 import servers
+
+list_servers = copy.deepcopy(servers.list_servers)
+get_server = copy.deepcopy(servers.get_server)
+list_servers_detail = copy.deepcopy(servers.list_servers_detail)
+
+# NOTE: The consolidated remote console API got introduced with v2.6
+# with bp/consolidate-console-api. See Nova commit 578bafeda
+get_remote_consoles = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'remote_console': {
+                'type': 'object',
+                'properties': {
+                    'protocol': {'enum': ['vnc', 'rdp', 'serial', 'spice']},
+                    'type': {'enum': ['novnc', 'xpvnc', 'rdp-html5',
+                                      'spice-html5', 'serial']},
+                    'url': {
+                        'type': 'string',
+                        'format': 'uri'
+                    }
+                },
+                'additionalProperties': False,
+                'required': ['protocol', 'type', 'url']
+            }
+        },
+        'additionalProperties': False,
+        'required': ['remote_console']
+    }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_9/servers.py b/tempest/lib/api_schema/response/compute/v2_9/servers.py
index 470190c..e260e48 100644
--- a/tempest/lib/api_schema/response/compute/v2_9/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_9/servers.py
@@ -14,7 +14,7 @@
 
 import copy
 
-from tempest.lib.api_schema.response.compute.v2_3 import servers
+from tempest.lib.api_schema.response.compute.v2_6 import servers
 
 list_servers = copy.deepcopy(servers.list_servers)
 
diff --git a/tempest/lib/services/compute/servers_client.py b/tempest/lib/services/compute/servers_client.py
index 0d355a1..ff65b25 100644
--- a/tempest/lib/services/compute/servers_client.py
+++ b/tempest/lib/services/compute/servers_client.py
@@ -27,6 +27,7 @@
 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_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
 from tempest.lib.services.compute import base_compute_client
@@ -37,7 +38,8 @@
 
     schema_versions_info = [
         {'min': None, 'max': '2.2', 'schema': schema},
-        {'min': '2.3', 'max': '2.8', 'schema': schemav23},
+        {'min': '2.3', 'max': '2.5', 'schema': schemav23},
+        {'min': '2.6', 'max': '2.8', 'schema': schemav26},
         {'min': '2.9', 'max': '2.15', 'schema': schemav29},
         {'min': '2.16', 'max': '2.18', 'schema': schemav216},
         {'min': '2.19', 'max': '2.25', 'schema': schemav219},
@@ -598,6 +600,29 @@
         return self.action(server_id, 'os-getConsoleOutput',
                            schema.get_console_output, **kwargs)
 
+    def get_remote_console(self, server_id, console_type, protocol, **kwargs):
+        """Get a remote console.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        TODO (markus_z) The api-ref for that isn't yet available, update this
+        here when the docs in Nova are updated. The old API is at
+        http://developer.openstack.org/api-ref/compute/#get-serial-console-os-getserialconsole-action
+        """
+        param = {
+            'remote_console': {
+                'type': console_type,
+                'protocol': protocol,
+            }
+        }
+        post_body = json.dumps(param)
+        resp, body = self.post("servers/%s/remote-consoles" % server_id,
+                               post_body)
+        body = json.loads(body)
+        schema = self.get_schema(self.schema_versions_info)
+        self.validate_response(schema.get_remote_consoles, resp, body)
+        return rest_client.ResponseBody(resp, body)
+
     def list_virtual_interfaces(self, server_id):
         """List the virtual interfaces used in an instance."""
         resp, body = self.get('/'.join(['servers', server_id,
diff --git a/tempest/lib/services/identity/v3/__init__.py b/tempest/lib/services/identity/v3/__init__.py
index f2f3391..6f498d9 100644
--- a/tempest/lib/services/identity/v3/__init__.py
+++ b/tempest/lib/services/identity/v3/__init__.py
@@ -17,6 +17,8 @@
 from tempest.lib.services.identity.v3.domain_configuration_client \
     import DomainConfigurationClient
 from tempest.lib.services.identity.v3.domains_client import DomainsClient
+from tempest.lib.services.identity.v3.endpoint_filter_client import \
+    EndPointsFilterClient
 from tempest.lib.services.identity.v3.endpoints_client import EndPointsClient
 from tempest.lib.services.identity.v3.groups_client import GroupsClient
 from tempest.lib.services.identity.v3.identity_client import IdentityClient
@@ -37,8 +39,8 @@
 from tempest.lib.services.identity.v3.versions_client import VersionsClient
 
 __all__ = ['CredentialsClient', 'DomainsClient', 'DomainConfigurationClient',
-           'EndPointsClient', 'GroupsClient', 'IdentityClient',
-           'InheritedRolesClient', 'OAUTHConsumerClient', 'PoliciesClient',
-           'ProjectsClient', 'RegionsClient', 'RoleAssignmentsClient',
-           'RolesClient', 'ServicesClient', 'V3TokenClient', 'TrustsClient',
-           'UsersClient', 'VersionsClient']
+           'EndPointsClient', 'EndPointsFilterClient', 'GroupsClient',
+           'IdentityClient', 'InheritedRolesClient', 'OAUTHConsumerClient',
+           'PoliciesClient', 'ProjectsClient', 'RegionsClient',
+           'RoleAssignmentsClient', 'RolesClient', 'ServicesClient',
+           'V3TokenClient', 'TrustsClient', 'UsersClient', 'VersionsClient']
diff --git a/tempest/lib/services/identity/v3/endpoint_filter_client.py b/tempest/lib/services/identity/v3/endpoint_filter_client.py
new file mode 100644
index 0000000..a8cd722
--- /dev/null
+++ b/tempest/lib/services/identity/v3/endpoint_filter_client.py
@@ -0,0 +1,68 @@
+# Copyright 2017 AT&T Corp.
+# 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.
+
+"""
+https://developer.openstack.org/api-ref/identity/v3-ext/#os-ep-filter-api
+"""
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class EndPointsFilterClient(rest_client.RestClient):
+    api_version = "v3"
+    ep_filter = "OS-EP-FILTER"
+
+    def list_projects_for_endpoint(self, endpoint_id):
+        """List all projects that are associated with the endpoint."""
+        resp, body = self.get(self.ep_filter + '/endpoints/%s/projects' %
+                              endpoint_id)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def add_endpoint_to_project(self, project_id, endpoint_id):
+        """Add association between project and endpoint. """
+        body = None
+        resp, body = self.put(
+            self.ep_filter + '/projects/%s/endpoints/%s' %
+            (project_id, endpoint_id), body)
+        self.expected_success(204, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def check_endpoint_in_project(self, project_id, endpoint_id):
+        """Check association of Project with Endpoint."""
+        resp, body = self.head(
+            self.ep_filter + '/projects/%s/endpoints/%s' %
+            (project_id, endpoint_id), None)
+        self.expected_success(204, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def list_endpoints_in_project(self, project_id):
+        """List Endpoints associated with Project."""
+        resp, body = self.get(self.ep_filter + '/projects/%s/endpoints'
+                              % project_id)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def delete_endpoint_from_project(self, project_id, endpoint_id):
+        """Delete association between project and endpoint."""
+        resp, body = self.delete(
+            self.ep_filter + '/projects/%s/endpoints/%s'
+            % (project_id, endpoint_id))
+        self.expected_success(204, resp.status)
+        return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v2/volumes_client.py b/tempest/lib/services/volume/v2/volumes_client.py
index 43fc9b8..8b5c96f 100644
--- a/tempest/lib/services/volume/v2/volumes_client.py
+++ b/tempest/lib/services/volume/v2/volumes_client.py
@@ -286,6 +286,19 @@
         resp, body = self.post('volumes/%s/action' % volume_id, post_body)
         self.expected_success(202, resp.status)
 
+    def force_detach_volume(self, volume_id, **kwargs):
+        """Force detach a volume.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v2/#force-detach-volume
+        """
+        post_body = json.dumps({'os-force_detach': kwargs})
+        url = 'volumes/%s/action' % volume_id
+        resp, body = self.post(url, post_body)
+        self.expected_success(202, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
     def update_volume_image_metadata(self, volume_id, **kwargs):
         """Update image metadata for the volume.
 
diff --git a/tempest/tests/cmd/test_workspace.py b/tempest/tests/cmd/test_workspace.py
index 6ca4d42..dc6c0c8 100644
--- a/tempest/tests/cmd/test_workspace.py
+++ b/tempest/tests/cmd/test_workspace.py
@@ -47,23 +47,25 @@
         self.assertEqual(return_code, expected, msg)
 
     def test_run_workspace_list(self):
-        cmd = ['tempest', 'workspace', '--workspace-path',
-               self.store_file, 'list']
+        cmd = ['tempest', 'workspace', 'list',
+               '--workspace-path', self.store_file]
         self._run_cmd_gets_return_code(cmd, 0)
 
     def test_run_workspace_register(self):
         name = data_utils.rand_uuid()
         path = tempfile.mkdtemp()
         self.addCleanup(shutil.rmtree, path, ignore_errors=True)
-        cmd = ['tempest', 'workspace', '--workspace-path', self.store_file,
-               'register', '--name', name, '--path', path]
+        cmd = ['tempest', 'workspace', 'register',
+               '--workspace-path', self.store_file,
+               '--name', name, '--path', path]
         self._run_cmd_gets_return_code(cmd, 0)
         self.assertIsNotNone(self.workspace_manager.get_workspace(name))
 
     def test_run_workspace_rename(self):
         new_name = data_utils.rand_uuid()
-        cmd = ['tempest', 'workspace', '--workspace-path', self.store_file,
-               'rename', "--old-name", self.name, '--new-name', new_name]
+        cmd = ['tempest', 'workspace', 'rename',
+               '--workspace-path', self.store_file,
+               '--old-name', self.name, '--new-name', new_name]
         self._run_cmd_gets_return_code(cmd, 0)
         self.assertIsNone(self.workspace_manager.get_workspace(self.name))
         self.assertIsNotNone(self.workspace_manager.get_workspace(new_name))
@@ -71,15 +73,17 @@
     def test_run_workspace_move(self):
         new_path = tempfile.mkdtemp()
         self.addCleanup(shutil.rmtree, new_path, ignore_errors=True)
-        cmd = ['tempest', 'workspace', '--workspace-path', self.store_file,
-               'move', '--name', self.name, '--path', new_path]
+        cmd = ['tempest', 'workspace', 'move',
+               '--workspace-path', self.store_file,
+               '--name', self.name, '--path', new_path]
         self._run_cmd_gets_return_code(cmd, 0)
         self.assertEqual(
             self.workspace_manager.get_workspace(self.name), new_path)
 
     def test_run_workspace_remove(self):
-        cmd = ['tempest', 'workspace', '--workspace-path', self.store_file,
-               'remove', '--name', self.name]
+        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))
 
diff --git a/tempest/tests/lib/services/compute/test_servers_client.py b/tempest/tests/lib/services/compute/test_servers_client.py
index a277dfe..a857329 100644
--- a/tempest/tests/lib/services/compute/test_servers_client.py
+++ b/tempest/tests/lib/services/compute/test_servers_client.py
@@ -1168,3 +1168,34 @@
             tag=self.FAKE_TAGS[0],
             status=204,
             to_utf=bytes_body)
+
+
+class TestServersClientMinV26(base.BaseServiceTest):
+
+    def setUp(self):
+        super(TestServersClientMinV26, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = servers_client.ServersClient(fake_auth, 'compute',
+                                                   'regionOne')
+        base_compute_client.COMPUTE_MICROVERSION = '2.6'
+        self.server_id = "920eaac8-a284-4fd1-9c2c-b30f0181b125"
+
+    def tearDown(self):
+        super(TestServersClientMinV26, self).tearDown()
+        base_compute_client.COMPUTE_MICROVERSION = None
+
+    def test_get_remote_consoles(self):
+        self.check_service_client_function(
+            self.client.get_remote_console,
+            'tempest.lib.common.rest_client.RestClient.post',
+            {
+                'remote_console': {
+                    'protocol': 'serial',
+                    'type': 'serial',
+                    'url': 'ws://127.0.0.1:6083/?token=IllAllowIt'
+                    }
+            },
+            server_id=self.server_id,
+            console_type='serial',
+            protocol='serial',
+            )
diff --git a/tempest/tests/lib/services/identity/v3/test_endpoint_filter_client.py b/tempest/tests/lib/services/identity/v3/test_endpoint_filter_client.py
new file mode 100644
index 0000000..7faf6a0
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_endpoint_filter_client.py
@@ -0,0 +1,165 @@
+# Copyright 2017 AT&T Corp.
+# 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.identity.v3 import endpoint_filter_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestEndPointsFilterClient(base.BaseServiceTest):
+    FAKE_LIST_PROJECTS_FOR_ENDPOINTS = {
+        "projects": [
+            {
+                "domain_id": "1777c7",
+                "enabled": True,
+                "id": "1234ab1",
+                "type": "compute",
+                "links": {
+                    "self": "http://example.com/identity/v3/projects/1234ab1"
+                },
+                "name": "Project 1",
+                "description": "Project 1 description",
+            },
+            {
+                "domain_id": "1777c7",
+                "enabled": True,
+                "id": "5678cd2",
+                "type": "compute",
+                "links": {
+                    "self": "http://example.com/identity/v3/projects/5678cd2"
+                },
+                "name": "Project 2",
+                "description": "Project 2 description",
+            }
+        ],
+        "links": {
+            "self": "http://example.com/identity/v3/OS-EP-FILTER/endpoints/\
+                    u6ay5u/projects",
+            "previous": None,
+            "next": None
+        }
+    }
+
+    FAKE_LIST_ENDPOINTS_FOR_PROJECTS = {
+        "endpoints": [
+            {
+                "id": "u6ay5u",
+                "interface": "public",
+                "url": "http://example.com/identity/",
+                "region": "north",
+                "links": {
+                    "self": "http://example.com/identity/v3/endpoints/u6ay5u"
+                },
+                "service_id": "5um4r",
+            },
+            {
+                "id": "u6ay5u",
+                "interface": "internal",
+                "url": "http://example.com/identity/",
+                "region": "south",
+                "links": {
+                    "self": "http://example.com/identity/v3/endpoints/u6ay5u"
+                },
+                "service_id": "5um4r",
+            },
+        ],
+        "links": {
+            "self": "http://example.com/identity/v3/OS-EP-FILTER/projects/\
+                    1234ab1/endpoints",
+            "previous": None,
+            "next": None
+        }
+    }
+
+    def setUp(self):
+        super(TestEndPointsFilterClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = endpoint_filter_client.EndPointsFilterClient(
+            fake_auth, 'identity', 'regionOne')
+
+    def _test_add_endpoint_to_project(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.add_endpoint_to_project,
+            'tempest.lib.common.rest_client.RestClient.put',
+            {},
+            bytes_body,
+            status=204,
+            project_id=3,
+            endpoint_id=4)
+
+    def _test_check_endpoint_in_project(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.check_endpoint_in_project,
+            'tempest.lib.common.rest_client.RestClient.head',
+            {},
+            bytes_body,
+            status=204,
+            project_id=3,
+            endpoint_id=4)
+
+    def _test_list_projects_for_endpoint(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_projects_for_endpoint,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_LIST_PROJECTS_FOR_ENDPOINTS,
+            bytes_body,
+            status=200,
+            endpoint_id=3)
+
+    def _test_list_endpoints_in_project(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_endpoints_in_project,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_LIST_ENDPOINTS_FOR_PROJECTS,
+            bytes_body,
+            status=200,
+            project_id=4)
+
+    def _test_delete_endpoint_from_project(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.delete_endpoint_from_project,
+            'tempest.lib.common.rest_client.RestClient.delete',
+            {},
+            bytes_body,
+            status=204,
+            project_id=3,
+            endpoint_id=4)
+
+    def test_add_endpoint_to_project_with_str_body(self):
+        self._test_add_endpoint_to_project()
+
+    def test_add_endpoint_to_project_with_bytes_body(self):
+        self._test_add_endpoint_to_project(bytes_body=True)
+
+    def test_check_endpoint_in_project_with_str_body(self):
+        self._test_check_endpoint_in_project()
+
+    def test_check_endpoint_in_project_with_bytes_body(self):
+        self._test_check_endpoint_in_project(bytes_body=True)
+
+    def test_list_projects_for_endpoint_with_str_body(self):
+        self._test_list_projects_for_endpoint()
+
+    def test_list_projects_for_endpoint_with_bytes_body(self):
+        self._test_list_projects_for_endpoint(bytes_body=True)
+
+    def test_list_endpoints_in_project_with_str_body(self):
+        self._test_list_endpoints_in_project()
+
+    def test_list_endpoints_in_project_with_bytes_body(self):
+        self._test_list_endpoints_in_project(bytes_body=True)
+
+    def test_delete_endpoint_from_project(self):
+        self._test_delete_endpoint_from_project()
diff --git a/tempest/tests/lib/services/volume/v2/test_volumes_client.py b/tempest/tests/lib/services/volume/v2/test_volumes_client.py
new file mode 100644
index 0000000..498b963
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v2/test_volumes_client.py
@@ -0,0 +1,52 @@
+# Copyright 2017 FiberHome Telecommunication Technologies CO.,LTD
+# 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 volumes_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestVolumesClient(base.BaseServiceTest):
+
+    def setUp(self):
+        super(TestVolumesClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = volumes_client.VolumesClient(fake_auth,
+                                                   'volume',
+                                                   'regionOne')
+
+    def _test_force_detach_volume(self, bytes_body=False):
+        kwargs = {
+            'attachment_id': '6980e295-920f-412e-b189-05c50d605acd',
+            'connector': {
+                'initiator': 'iqn.2017-04.org.fake:01'
+            }
+        }
+
+        self.check_service_client_function(
+            self.client.force_detach_volume,
+            'tempest.lib.common.rest_client.RestClient.post',
+            {},
+            to_utf=bytes_body,
+            status=202,
+            volume_id="a3be971b-8de5-4bdf-bdb8-3d8eb0fb69f8",
+            **kwargs
+        )
+
+    def test_force_detach_volume_with_str_body(self):
+        self._test_force_detach_volume()
+
+    def test_force_detach_volume_with_bytes_body(self):
+        self._test_force_detach_volume(bytes_body=True)