Merge "Stop passing TestResources to Managers"
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 7e4503d..c2df0b6 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -135,7 +135,7 @@
 
 # If true, SmartyPants will be used to convert quotes and dashes to
 # typographically correct entities.
-#html_use_smartypants = True
+html_use_smartypants = False
 
 # Custom sidebar templates, maps document names to template names.
 #html_sidebars = {}
diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst
index 32cd3ef..e4b104f 100644
--- a/doc/source/configuration.rst
+++ b/doc/source/configuration.rst
@@ -26,6 +26,11 @@
 - Run tests for admin APIs
 - Generate test credentials on the fly (see `Dynamic Credentials`_)
 
+When keystone uses a policy that requires domain scoped tokens for admin
+actions, the flag ``admin_domain_scope`` must be set to ``True``.
+The admin user configured, if any, must have a role assigned to the domain to
+be usable.
+
 Tempest allows for configuring pre-provisioned test credentials as well.
 This can be done using the accounts.yaml file (see
 `Pre-Provisioned Credentials`_). This file is used to specify an arbitrary
@@ -87,6 +92,14 @@
 by dynamic credentials. This option will not have any effect when Tempest is not
 configured to use dynamic credentials.
 
+When the ``admin_domain_scope`` option is set to ``True``, provisioned admin
+accounts will be assigned a role on domain configured in
+``default_credentials_domain_name``. This will make the accounts provisioned
+usable in a cloud where domain scoped tokens are required by keystone for
+admin operations. Note that the the initial pre-provision admin accounts,
+configured in tempest.conf, must have a role on the same domain as well, for
+Dynamic Credentials to work.
+
 
 Pre-Provisioned Credentials
 """""""""""""""""""""""""""
@@ -124,6 +137,18 @@
 to the tests using the credentials, and failure to do this will likely cause
 unexpected failures in some tests.
 
+When the keystone in the target cloud requires domain scoped tokens to
+perform admin actions, all pre-provisioned admin users must have a role
+assigned on the domain where test accounts a provisioned.
+The option ``admin_domain_scope`` is used to tell tempest that domain scoped
+tokens shall be used. ``default_credentials_domain_name`` is the domain where
+test accounts are expected to be provisioned if no domain is specified.
+
+Note that if credentials are pre-provisioned via ``tempest account-generator``
+the role on the domain will be assigned automatically for you, as long as
+``admin_domain_scope`` as ``default_credentials_domain_name`` are configured
+properly in tempest.conf.
+
 Pre-Provisioned Credentials are also know as accounts.yaml or accounts file.
 
 Compute
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 10364db..c73fac3 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -51,6 +51,8 @@
    account_generator
    cleanup
    javelin
+   workspace
+   run
 
 ==================
 Indices and tables
diff --git a/doc/source/library.rst b/doc/source/library.rst
index a89512c..6a2fb83 100644
--- a/doc/source/library.rst
+++ b/doc/source/library.rst
@@ -66,3 +66,4 @@
    library/rest_client
    library/utils
    library/api_microversion_testing
+   library/auth
diff --git a/doc/source/microversion_testing.rst b/doc/source/microversion_testing.rst
index 3568470..17059e4 100644
--- a/doc/source/microversion_testing.rst
+++ b/doc/source/microversion_testing.rst
@@ -19,6 +19,7 @@
   multiple Microversion tests in a single Tempest operation, configuration
   options should represent the range of test target Microversions.
   New configuration options are:
+
   * min_microversion
   * max_microversion
 
@@ -130,8 +131,9 @@
 If that range is out of configured Microversion range then, test
 will be skipped.
 
-*NOTE: Microversion testing is supported at test class level not at individual
-test case level.*
+.. note:: Microversion testing is supported at test class level not at
+      individual test case level.
+
 For example:
 
 Below test is applicable for Microversion from 2.2 till 2.9::
@@ -211,3 +213,7 @@
  * `2.10`_
 
  .. _2.10: http://docs.openstack.org/developer/nova/api_microversion_history.html#id9
+
+ * `2.20`_
+
+ .. _2.20: http://docs.openstack.org/developer/nova/api_microversion_history.html#id18
diff --git a/doc/source/run.rst b/doc/source/run.rst
new file mode 100644
index 0000000..07fa5f7
--- /dev/null
+++ b/doc/source/run.rst
@@ -0,0 +1,5 @@
+-----------
+Tempest Run
+-----------
+
+.. automodule:: tempest.cmd.run
diff --git a/doc/source/workspace.rst b/doc/source/workspace.rst
new file mode 100644
index 0000000..41325b2
--- /dev/null
+++ b/doc/source/workspace.rst
@@ -0,0 +1,5 @@
+-----------------
+Tempest Workspace
+-----------------
+
+.. automodule:: tempest.cmd.workspace
diff --git a/releasenotes/notes/add-tempest-run-3d0aaf69c2ca4115.yaml b/releasenotes/notes/add-tempest-run-3d0aaf69c2ca4115.yaml
new file mode 100644
index 0000000..429bf52
--- /dev/null
+++ b/releasenotes/notes/add-tempest-run-3d0aaf69c2ca4115.yaml
@@ -0,0 +1,4 @@
+---
+features:
+  - Adds the tempest run command to the unified tempest CLI. This new command
+    is used for running tempest tests.
diff --git a/releasenotes/notes/add-tempest-workspaces-228a2ba4690b5589.yaml b/releasenotes/notes/add-tempest-workspaces-228a2ba4690b5589.yaml
new file mode 100644
index 0000000..9a1cef6
--- /dev/null
+++ b/releasenotes/notes/add-tempest-workspaces-228a2ba4690b5589.yaml
@@ -0,0 +1,5 @@
+---
+features:
+  - Adds tempest workspaces command and WorkspaceManager.
+    This is used to have a centralized repository for managing
+    different tempest configurations.
diff --git a/releasenotes/notes/bug-1486834-7ebca15836ae27a9.yaml b/releasenotes/notes/bug-1486834-7ebca15836ae27a9.yaml
new file mode 100644
index 0000000..b2190f3
--- /dev/null
+++ b/releasenotes/notes/bug-1486834-7ebca15836ae27a9.yaml
@@ -0,0 +1,7 @@
+---
+features:
+  - |
+    Tempest library auth interface now supports
+    filtering with catalog name.  Note that filtering by
+    name is only successful if a known service type is
+    provided.
diff --git a/releasenotes/notes/image-clients-as-library-86d17caa26ce3961.yaml b/releasenotes/notes/image-clients-as-library-86d17caa26ce3961.yaml
index 630f8ed..b50ed38 100644
--- a/releasenotes/notes/image-clients-as-library-86d17caa26ce3961.yaml
+++ b/releasenotes/notes/image-clients-as-library-86d17caa26ce3961.yaml
@@ -1,10 +1,12 @@
 ---
 features:
-  - Define image service clients as libraries
+  - |
+    Define image service clients as libraries
     The following image service clients are defined as library interface,
     so the other projects can use these modules as stable libraries
     without any maintenance changes.
-      **image_members_client**
-      **namespaces_client**
-      **resource_types_client**
-      **schemas_client**
+
+      * image_members_client
+      * namespaces_client
+      * resource_types_client
+      * schemas_client
diff --git a/setup.cfg b/setup.cfg
index 24e0214..66a8743 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -41,6 +41,8 @@
     run-stress = tempest.cmd.run_stress:TempestRunStress
     list-plugins = tempest.cmd.list_plugins:TempestListPlugins
     verify-config = tempest.cmd.verify_tempest_config:TempestVerifyConfig
+    workspace = tempest.cmd.workspace:TempestWorkspace
+    run = tempest.cmd.run:TempestRun
 oslo.config.opts =
     tempest.config = tempest.config:list_opts
 
diff --git a/tempest/api/compute/admin/test_aggregates.py b/tempest/api/compute/admin/test_aggregates.py
index 25efd2e..dabc45e 100644
--- a/tempest/api/compute/admin/test_aggregates.py
+++ b/tempest/api/compute/admin/test_aggregates.py
@@ -13,6 +13,8 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import testtools
+
 from tempest.api.compute import base
 from tempest.common import tempest_fixtures as fixtures
 from tempest.common.utils import data_utils
@@ -36,10 +38,16 @@
         cls.aggregate_name_prefix = 'test_aggregate'
         cls.az_name_prefix = 'test_az'
 
-        hosts_all = cls.os_adm.hosts_client.list_hosts()['hosts']
-        hosts = map(lambda x: x['host_name'],
-                    filter(lambda y: y['service'] == 'compute', hosts_all))
-        cls.host = hosts[0]
+        cls.host = None
+        hypers = cls.os_adm.hypervisor_client.list_hypervisors()['hypervisors']
+        hypers_available = [hyper['hypervisor_hostname'] for hyper in hypers
+                            if (hyper['state'] == 'up' and
+                                hyper['status'] == 'enabled')]
+        if hypers_available:
+            cls.host = hypers_available[0]
+        else:
+            raise testtools.TestCase.failureException(
+                "no available compute node found")
 
     @test.idempotent_id('0d148aa3-d54c-4317-aa8d-42040a475e20')
     def test_aggregate_create_delete(self):
diff --git a/tempest/api/compute/volumes/test_attach_volume.py b/tempest/api/compute/volumes/test_attach_volume.py
index fa3fdfe..05c23ee 100644
--- a/tempest/api/compute/volumes/test_attach_volume.py
+++ b/tempest/api/compute/volumes/test_attach_volume.py
@@ -200,7 +200,7 @@
             server=self.server,
             servers_client=self.servers_client)
 
-        command = 'grep vd /proc/partitions | wc -l'
+        command = 'grep [vs]d /proc/partitions | wc -l'
         nb_partitions = linux_client.exec_command(command).strip()
         self.assertEqual(number_of_partition, nb_partitions)
 
diff --git a/tempest/api/identity/admin/v3/test_trusts.py b/tempest/api/identity/admin/v3/test_trusts.py
index 09ae468..9c8f1f6 100644
--- a/tempest/api/identity/admin/v3/test_trusts.py
+++ b/tempest/api/identity/admin/v3/test_trusts.py
@@ -98,7 +98,8 @@
             password=self.trustor_password,
             user_domain_id='default',
             tenant_name=self.trustor_project_name,
-            project_domain_id='default')
+            project_domain_id='default',
+            domain_id='default')
         os = clients.Manager(credentials=creds)
         self.trustor_client = os.trusts_client
 
@@ -266,7 +267,18 @@
     @test.attr(type='smoke')
     @test.idempotent_id('4773ebd5-ecbf-4255-b8d8-b63e6f72b65d')
     def test_get_trusts_all(self):
+
+        # Simple function that can be used for cleanup
+        def set_scope(auth_provider, scope):
+            auth_provider.scope = scope
+
         self.create_trust()
+        # Listing trusts can be done by trustor, by trustee, or without
+        # any filter if scoped to a project, so we must ensure token scope is
+        # project for this test.
+        original_scope = self.os_adm.auth_provider.scope
+        set_scope(self.os_adm.auth_provider, 'project')
+        self.addCleanup(set_scope, self.os_adm.auth_provider, original_scope)
         trusts_get = self.trusts_client.list_trusts()['trusts']
         trusts = [t for t in trusts_get
                   if t['id'] == self.trust_id]
diff --git a/tempest/api/identity/base.py b/tempest/api/identity/base.py
index 2d4ff17..31420d1 100644
--- a/tempest/api/identity/base.py
+++ b/tempest/api/identity/base.py
@@ -162,6 +162,12 @@
         cls.creds_client = cls.os_adm.credentials_client
         cls.groups_client = cls.os_adm.groups_client
         cls.projects_client = cls.os_adm.projects_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.
+            # As the client manager is already created by the base class,
+            # we set the scope for the inner auth provider.
+            cls.os_adm.auth_provider.scope = 'domain'
 
     @classmethod
     def resource_setup(cls):
diff --git a/tempest/api/image/base.py b/tempest/api/image/base.py
index 307d6d6..3fefc81 100644
--- a/tempest/api/image/base.py
+++ b/tempest/api/image/base.py
@@ -137,7 +137,7 @@
     @classmethod
     def setup_clients(cls):
         super(BaseV2MemberImageTest, cls).setup_clients()
-        cls.os_image_member_client = cls.os.image_member_client_v2
+        cls.image_member_client = cls.os.image_member_client_v2
         cls.alt_image_member_client = cls.os_alt.image_member_client_v2
         cls.alt_img_client = cls.os_alt.image_client_v2
 
diff --git a/tempest/api/image/v2/test_images_member.py b/tempest/api/image/v2/test_images_member.py
index d8254f5..fe8dd65 100644
--- a/tempest/api/image/v2/test_images_member.py
+++ b/tempest/api/image/v2/test_images_member.py
@@ -19,7 +19,7 @@
     @test.idempotent_id('5934c6ea-27dc-4d6e-9421-eeb5e045494a')
     def test_image_share_accept(self):
         image_id = self._create_image()
-        member = self.os_image_member_client.create_image_member(
+        member = self.image_member_client.create_image_member(
             image_id, member=self.alt_tenant_id)
         self.assertEqual(member['member_id'], self.alt_tenant_id)
         self.assertEqual(member['image_id'], image_id)
@@ -29,7 +29,7 @@
                                                          self.alt_tenant_id,
                                                          status='accepted')
         self.assertIn(image_id, self._list_image_ids_as_alt())
-        body = self.os_image_member_client.list_image_members(image_id)
+        body = self.image_member_client.list_image_members(image_id)
         members = body['members']
         member = members[0]
         self.assertEqual(len(members), 1, str(members))
@@ -40,7 +40,7 @@
     @test.idempotent_id('d9e83e5f-3524-4b38-a900-22abcb26e90e')
     def test_image_share_reject(self):
         image_id = self._create_image()
-        member = self.os_image_member_client.create_image_member(
+        member = self.image_member_client.create_image_member(
             image_id, member=self.alt_tenant_id)
         self.assertEqual(member['member_id'], self.alt_tenant_id)
         self.assertEqual(member['image_id'], image_id)
@@ -54,14 +54,14 @@
     @test.idempotent_id('a6ee18b9-4378-465e-9ad9-9a6de58a3287')
     def test_get_image_member(self):
         image_id = self._create_image()
-        self.os_image_member_client.create_image_member(
+        self.image_member_client.create_image_member(
             image_id, member=self.alt_tenant_id)
         self.alt_image_member_client.update_image_member(image_id,
                                                          self.alt_tenant_id,
                                                          status='accepted')
 
         self.assertIn(image_id, self._list_image_ids_as_alt())
-        member = self.os_image_member_client.show_image_member(
+        member = self.image_member_client.show_image_member(
             image_id, self.alt_tenant_id)
         self.assertEqual(self.alt_tenant_id, member['member_id'])
         self.assertEqual(image_id, member['image_id'])
@@ -70,15 +70,15 @@
     @test.idempotent_id('72989bc7-2268-48ed-af22-8821e835c914')
     def test_remove_image_member(self):
         image_id = self._create_image()
-        self.os_image_member_client.create_image_member(
+        self.image_member_client.create_image_member(
             image_id, member=self.alt_tenant_id)
         self.alt_image_member_client.update_image_member(image_id,
                                                          self.alt_tenant_id,
                                                          status='accepted')
 
         self.assertIn(image_id, self._list_image_ids_as_alt())
-        self.os_image_member_client.delete_image_member(image_id,
-                                                        self.alt_tenant_id)
+        self.image_member_client.delete_image_member(image_id,
+                                                     self.alt_tenant_id)
         self.assertNotIn(image_id, self._list_image_ids_as_alt())
 
     @test.idempotent_id('634dcc3f-f6e2-4409-b8fd-354a0bb25d83')
@@ -94,7 +94,7 @@
     @test.idempotent_id('cb961424-3f68-4d21-8e36-30ad66fb6bfb')
     def test_get_private_image(self):
         image_id = self._create_image()
-        member = self.os_image_member_client.create_image_member(
+        member = self.image_member_client.create_image_member(
             image_id, member=self.alt_tenant_id)
         self.assertEqual(member['member_id'], self.alt_tenant_id)
         self.assertEqual(member['image_id'], image_id)
@@ -104,6 +104,6 @@
                                                          self.alt_tenant_id,
                                                          status='accepted')
         self.assertIn(image_id, self._list_image_ids_as_alt())
-        self.os_image_member_client.delete_image_member(image_id,
-                                                        self.alt_tenant_id)
+        self.image_member_client.delete_image_member(image_id,
+                                                     self.alt_tenant_id)
         self.assertNotIn(image_id, self._list_image_ids_as_alt())
diff --git a/tempest/api/image/v2/test_images_member_negative.py b/tempest/api/image/v2/test_images_member_negative.py
index 5a08d77..fa29a92 100644
--- a/tempest/api/image/v2/test_images_member_negative.py
+++ b/tempest/api/image/v2/test_images_member_negative.py
@@ -21,7 +21,7 @@
     @test.idempotent_id('b79efb37-820d-4cf0-b54c-308b00cf842c')
     def test_image_share_invalid_status(self):
         image_id = self._create_image()
-        member = self.os_image_member_client.create_image_member(
+        member = self.image_member_client.create_image_member(
             image_id, member=self.alt_tenant_id)
         self.assertEqual(member['status'], 'pending')
         self.assertRaises(lib_exc.BadRequest,
@@ -33,11 +33,11 @@
     @test.idempotent_id('27002f74-109e-4a37-acd0-f91cd4597967')
     def test_image_share_owner_cannot_accept(self):
         image_id = self._create_image()
-        member = self.os_image_member_client.create_image_member(
+        member = self.image_member_client.create_image_member(
             image_id, member=self.alt_tenant_id)
         self.assertEqual(member['status'], 'pending')
         self.assertNotIn(image_id, self._list_image_ids_as_alt())
         self.assertRaises(lib_exc.Forbidden,
-                          self.os_image_member_client.update_image_member,
+                          self.image_member_client.update_image_member,
                           image_id, self.alt_tenant_id, status='accepted')
         self.assertNotIn(image_id, self._list_image_ids_as_alt())
diff --git a/tempest/api/volume/admin/test_multi_backend.py b/tempest/api/volume/admin/test_multi_backend.py
index 00acc7d..5e1c20b 100644
--- a/tempest/api/volume/admin/test_multi_backend.py
+++ b/tempest/api/volume/admin/test_multi_backend.py
@@ -63,7 +63,7 @@
             extra_specs = {spec_key_with_prefix: backend_name_key}
         else:
             extra_specs = {spec_key_without_prefix: backend_name_key}
-        self.type = self.volume_types_client.create_volume_type(
+        self.type = self.admin_volume_types_client.create_volume_type(
             name=type_name, extra_specs=extra_specs)['volume_type']
         self.volume_type_id_list.append(self.type['id'])
 
@@ -95,7 +95,7 @@
         # volume types deletion
         volume_type_id_list = getattr(cls, 'volume_type_id_list', [])
         for volume_type_id in volume_type_id_list:
-            cls.volume_types_client.delete_volume_type(volume_type_id)
+            cls.admin_volume_types_client.delete_volume_type(volume_type_id)
 
         super(VolumeMultiBackendV2Test, cls).resource_cleanup()
 
diff --git a/tempest/api/volume/admin/test_qos.py b/tempest/api/volume/admin/test_qos.py
index 722a39a..68e57f5 100644
--- a/tempest/api/volume/admin/test_qos.py
+++ b/tempest/api/volume/admin/test_qos.py
@@ -43,27 +43,27 @@
         for key in ['name', 'consumer']:
             self.assertEqual(qos[key], body[key])
 
-        self.volume_qos_client.delete_qos(body['id'])
-        self.volume_qos_client.wait_for_resource_deletion(body['id'])
+        self.admin_volume_qos_client.delete_qos(body['id'])
+        self.admin_volume_qos_client.wait_for_resource_deletion(body['id'])
 
         # validate the deletion
-        list_qos = self.volume_qos_client.list_qos()['qos_specs']
+        list_qos = self.admin_volume_qos_client.list_qos()['qos_specs']
         self.assertNotIn(body, list_qos)
 
     def _create_test_volume_type(self):
         vol_type_name = utils.rand_name("volume-type")
-        vol_type = self.volume_types_client.create_volume_type(
+        vol_type = self.admin_volume_types_client.create_volume_type(
             name=vol_type_name)['volume_type']
-        self.addCleanup(self.volume_types_client.delete_volume_type,
+        self.addCleanup(self.admin_volume_types_client.delete_volume_type,
                         vol_type['id'])
         return vol_type
 
     def _test_associate_qos(self, vol_type_id):
-        self.volume_qos_client.associate_qos(
+        self.admin_volume_qos_client.associate_qos(
             self.created_qos['id'], vol_type_id)
 
     def _test_get_association_qos(self):
-        body = self.volume_qos_client.show_association_qos(
+        body = self.admin_volume_qos_client.show_association_qos(
             self.created_qos['id'])['qos_associations']
 
         associations = []
@@ -99,7 +99,7 @@
     @test.idempotent_id('7aa214cc-ac1a-4397-931f-3bb2e83bb0fd')
     def test_get_qos(self):
         """Tests the detail of a given qos-specs"""
-        body = self.volume_qos_client.show_qos(
+        body = self.admin_volume_qos_client.show_qos(
             self.created_qos['id'])['qos_specs']
         self.assertEqual(self.qos_name, body['name'])
         self.assertEqual(self.qos_consumer, body['consumer'])
@@ -107,28 +107,29 @@
     @test.idempotent_id('75e04226-bcf7-4595-a34b-fdf0736f38fc')
     def test_list_qos(self):
         """Tests the list of all qos-specs"""
-        body = self.volume_qos_client.list_qos()['qos_specs']
+        body = self.admin_volume_qos_client.list_qos()['qos_specs']
         self.assertIn(self.created_qos, body)
 
     @test.idempotent_id('ed00fd85-4494-45f2-8ceb-9e2048919aed')
     def test_set_unset_qos_key(self):
         """Test the addition of a specs key to qos-specs"""
         args = {'iops_bytes': '500'}
-        body = self.volume_qos_client.set_qos_key(
+        body = self.admin_volume_qos_client.set_qos_key(
             self.created_qos['id'],
             iops_bytes='500')['qos_specs']
         self.assertEqual(args, body)
-        body = self.volume_qos_client.show_qos(
+        body = self.admin_volume_qos_client.show_qos(
             self.created_qos['id'])['qos_specs']
         self.assertEqual(args['iops_bytes'], body['specs']['iops_bytes'])
 
         # test the deletion of a specs key from qos-specs
         keys = ['iops_bytes']
-        self.volume_qos_client.unset_qos_key(self.created_qos['id'], keys)
+        self.admin_volume_qos_client.unset_qos_key(self.created_qos['id'],
+                                                   keys)
         operation = 'qos-key-unset'
-        self.volume_qos_client.wait_for_qos_operations(self.created_qos['id'],
-                                                       operation, keys)
-        body = self.volume_qos_client.show_qos(
+        self.admin_volume_qos_client.wait_for_qos_operations(
+            self.created_qos['id'], operation, keys)
+        body = self.admin_volume_qos_client.show_qos(
             self.created_qos['id'])['qos_specs']
         self.assertNotIn(keys[0], body['specs'])
 
@@ -158,21 +159,20 @@
             self.assertIn(vol_type[i]['id'], associations)
 
         # disassociate a volume-type with qos-specs
-        self.volume_qos_client.disassociate_qos(
+        self.admin_volume_qos_client.disassociate_qos(
             self.created_qos['id'], vol_type[0]['id'])
         operation = 'disassociate'
-        self.volume_qos_client.wait_for_qos_operations(self.created_qos['id'],
-                                                       operation,
-                                                       vol_type[0]['id'])
+        self.admin_volume_qos_client.wait_for_qos_operations(
+            self.created_qos['id'], operation, vol_type[0]['id'])
         associations = self._test_get_association_qos()
         self.assertNotIn(vol_type[0]['id'], associations)
 
         # disassociate all volume-types from qos-specs
-        self.volume_qos_client.disassociate_all_qos(
+        self.admin_volume_qos_client.disassociate_all_qos(
             self.created_qos['id'])
         operation = 'disassociate-all'
-        self.volume_qos_client.wait_for_qos_operations(self.created_qos['id'],
-                                                       operation)
+        self.admin_volume_qos_client.wait_for_qos_operations(
+            self.created_qos['id'], operation)
         associations = self._test_get_association_qos()
         self.assertEmpty(associations)
 
diff --git a/tempest/api/volume/admin/test_volume_hosts.py b/tempest/api/volume/admin/test_volume_hosts.py
index b28488a..b58c525 100644
--- a/tempest/api/volume/admin/test_volume_hosts.py
+++ b/tempest/api/volume/admin/test_volume_hosts.py
@@ -21,7 +21,7 @@
 
     @test.idempotent_id('d5f3efa2-6684-4190-9ced-1c2f526352ad')
     def test_list_hosts(self):
-        hosts = self.hosts_client.list_hosts()['hosts']
+        hosts = self.admin_hosts_client.list_hosts()['hosts']
         self.assertTrue(len(hosts) >= 2, "No. of hosts are < 2,"
                         "response of list hosts is: % s" % hosts)
 
diff --git a/tempest/api/volume/admin/test_volume_quotas.py b/tempest/api/volume/admin/test_volume_quotas.py
index cf05f5f..27ccae5 100644
--- a/tempest/api/volume/admin/test_volume_quotas.py
+++ b/tempest/api/volume/admin/test_volume_quotas.py
@@ -37,14 +37,14 @@
 
     @test.idempotent_id('59eada70-403c-4cef-a2a3-a8ce2f1b07a0')
     def test_list_quotas(self):
-        quotas = (self.quotas_client.show_quota_set(self.demo_tenant_id)
+        quotas = (self.admin_quotas_client.show_quota_set(self.demo_tenant_id)
                   ['quota_set'])
         for key in QUOTA_KEYS:
             self.assertIn(key, quotas)
 
     @test.idempotent_id('2be020a2-5fdd-423d-8d35-a7ffbc36e9f7')
     def test_list_default_quotas(self):
-        quotas = self.quotas_client.show_default_quota_set(
+        quotas = self.admin_quotas_client.show_default_quota_set(
             self.demo_tenant_id)['quota_set']
         for key in QUOTA_KEYS:
             self.assertIn(key, quotas)
@@ -52,21 +52,21 @@
     @test.idempotent_id('3d45c99e-cc42-4424-a56e-5cbd212b63a6')
     def test_update_all_quota_resources_for_tenant(self):
         # Admin can update all the resource quota limits for a tenant
-        default_quota_set = self.quotas_client.show_default_quota_set(
+        default_quota_set = self.admin_quotas_client.show_default_quota_set(
             self.demo_tenant_id)['quota_set']
         new_quota_set = {'gigabytes': 1009,
                          'volumes': 11,
                          'snapshots': 11}
 
         # Update limits for all quota resources
-        quota_set = self.quotas_client.update_quota_set(
+        quota_set = self.admin_quotas_client.update_quota_set(
             self.demo_tenant_id,
             **new_quota_set)['quota_set']
 
         cleanup_quota_set = dict(
             (k, v) for k, v in six.iteritems(default_quota_set)
             if k in QUOTA_KEYS)
-        self.addCleanup(self.quotas_client.update_quota_set,
+        self.addCleanup(self.admin_quotas_client.update_quota_set,
                         self.demo_tenant_id, **cleanup_quota_set)
         # test that the specific values we set are actually in
         # the final result. There is nothing here that ensures there
@@ -75,7 +75,7 @@
 
     @test.idempotent_id('18c51ae9-cb03-48fc-b234-14a19374dbed')
     def test_show_quota_usage(self):
-        quota_usage = self.quotas_client.show_quota_usage(
+        quota_usage = self.admin_quotas_client.show_quota_usage(
             self.os_adm.credentials.tenant_id)['quota_set']
         for key in QUOTA_KEYS:
             self.assertIn(key, quota_usage)
@@ -84,13 +84,13 @@
 
     @test.idempotent_id('ae8b6091-48ad-4bfa-a188-bbf5cc02115f')
     def test_quota_usage(self):
-        quota_usage = self.quotas_client.show_quota_usage(
+        quota_usage = self.admin_quotas_client.show_quota_usage(
             self.demo_tenant_id)['quota_set']
 
         volume = self.create_volume()
         self.addCleanup(self._delete_volume, volume['id'])
 
-        new_quota_usage = self.quotas_client.show_quota_usage(
+        new_quota_usage = self.admin_quotas_client.show_quota_usage(
             self.demo_tenant_id)['quota_set']
 
         self.assertEqual(quota_usage['volumes']['in_use'] + 1,
@@ -109,15 +109,15 @@
                                                      description=description)
         project_id = project['id']
         self.addCleanup(self.identity_utils.delete_project, project_id)
-        quota_set_default = self.quotas_client.show_default_quota_set(
+        quota_set_default = self.admin_quotas_client.show_default_quota_set(
             project_id)['quota_set']
         volume_default = quota_set_default['volumes']
 
-        self.quotas_client.update_quota_set(project_id,
-                                            volumes=(int(volume_default) + 5))
+        self.admin_quotas_client.update_quota_set(
+            project_id, volumes=(int(volume_default) + 5))
 
-        self.quotas_client.delete_quota_set(project_id)
-        quota_set_new = (self.quotas_client.show_quota_set(project_id)
+        self.admin_quotas_client.delete_quota_set(project_id)
+        quota_set_new = (self.admin_quotas_client.show_quota_set(project_id)
                          ['quota_set'])
         self.assertEqual(volume_default, quota_set_new['volumes'])
 
diff --git a/tempest/api/volume/admin/test_volume_quotas_negative.py b/tempest/api/volume/admin/test_volume_quotas_negative.py
index a43ee8e..dde8915 100644
--- a/tempest/api/volume/admin/test_volume_quotas_negative.py
+++ b/tempest/api/volume/admin/test_volume_quotas_negative.py
@@ -38,7 +38,7 @@
 
         # NOTE(gfidente): no need to restore original quota set
         # after the tests as they only work with dynamic credentials.
-        cls.quotas_client.update_quota_set(
+        cls.admin_quotas_client.update_quota_set(
             cls.demo_tenant_id,
             **cls.shared_quota_set)
 
@@ -58,12 +58,12 @@
         # NOTE(gfidente): quota set needs to be changed for this test
         # or we may be limited by the volumes or snaps quota number, not by
         # actual gigs usage; next line ensures shared set is restored.
-        self.addCleanup(self.quotas_client.update_quota_set,
+        self.addCleanup(self.admin_quotas_client.update_quota_set,
                         self.demo_tenant_id,
                         **self.shared_quota_set)
         new_quota_set = {'gigabytes': self.default_volume_size,
                          'volumes': 2, 'snapshots': 1}
-        self.quotas_client.update_quota_set(
+        self.admin_quotas_client.update_quota_set(
             self.demo_tenant_id,
             **new_quota_set)
         self.assertRaises(lib_exc.OverLimit,
diff --git a/tempest/api/volume/admin/test_volume_snapshot_quotas_negative.py b/tempest/api/volume/admin/test_volume_snapshot_quotas_negative.py
index b7f70ba..1565a8c 100644
--- a/tempest/api/volume/admin/test_volume_snapshot_quotas_negative.py
+++ b/tempest/api/volume/admin/test_volume_snapshot_quotas_negative.py
@@ -44,7 +44,7 @@
 
         # NOTE(gfidente): no need to restore original quota set
         # after the tests as they only work with tenant isolation.
-        cls.quotas_client.update_quota_set(
+        cls.admin_quotas_client.update_quota_set(
             cls.demo_tenant_id,
             **cls.shared_quota_set)
 
@@ -63,12 +63,12 @@
     @test.attr(type='negative')
     @test.idempotent_id('c99a1ca9-6cdf-498d-9fdf-25832babef27')
     def test_quota_volume_gigabytes_snapshots(self):
-        self.addCleanup(self.quotas_client.update_quota_set,
+        self.addCleanup(self.admin_quotas_client.update_quota_set,
                         self.demo_tenant_id,
                         **self.shared_quota_set)
         new_quota_set = {'gigabytes': 2 * self.default_volume_size,
                          'volumes': 1, 'snapshots': 2}
-        self.quotas_client.update_quota_set(
+        self.admin_quotas_client.update_quota_set(
             self.demo_tenant_id,
             **new_quota_set)
         self.assertRaises(lib_exc.OverLimit,
diff --git a/tempest/api/volume/admin/test_volume_types.py b/tempest/api/volume/admin/test_volume_types.py
index 6fc6f1c..587dbd2 100644
--- a/tempest/api/volume/admin/test_volume_types.py
+++ b/tempest/api/volume/admin/test_volume_types.py
@@ -31,7 +31,8 @@
     @test.idempotent_id('9d9b28e3-1b2e-4483-a2cc-24aa0ea1de54')
     def test_volume_type_list(self):
         # List volume types.
-        body = self.volume_types_client.list_volume_types()['volume_types']
+        body = \
+            self.admin_volume_types_client.list_volume_types()['volume_types']
         self.assertIsInstance(body, list)
 
     @test.idempotent_id('c03cc62c-f4e9-4623-91ec-64ce2f9c1260')
@@ -47,11 +48,11 @@
         # Create two volume_types
         for i in range(2):
             vol_type_name = data_utils.rand_name("volume-type")
-            vol_type = self.volume_types_client.create_volume_type(
+            vol_type = self.admin_volume_types_client.create_volume_type(
                 name=vol_type_name,
                 extra_specs=extra_specs)['volume_type']
             volume_types.append(vol_type)
-            self.addCleanup(self.volume_types_client.delete_volume_type,
+            self.addCleanup(self.admin_volume_types_client.delete_volume_type,
                             vol_type['id'])
         params = {self.name_field: vol_name,
                   'volume_type': volume_types[0]['id']}
@@ -97,11 +98,11 @@
         vendor = CONF.volume.vendor_name
         extra_specs = {"storage_protocol": proto,
                        "vendor_name": vendor}
-        body = self.volume_types_client.create_volume_type(
+        body = self.admin_volume_types_client.create_volume_type(
             name=name,
             extra_specs=extra_specs)['volume_type']
         self.assertIn('id', body)
-        self.addCleanup(self.volume_types_client.delete_volume_type,
+        self.addCleanup(self.admin_volume_types_client.delete_volume_type,
                         body['id'])
         self.assertIn('name', body)
         self.assertEqual(body['name'], name,
@@ -109,7 +110,7 @@
                          "to the requested name")
         self.assertTrue(body['id'] is not None,
                         "Field volume_type id is empty or not found.")
-        fetched_volume_type = self.volume_types_client.show_volume_type(
+        fetched_volume_type = self.admin_volume_types_client.show_volume_type(
             body['id'])['volume_type']
         self.assertEqual(name, fetched_volume_type['name'],
                          'The fetched Volume_type is different '
@@ -127,15 +128,16 @@
         provider = "LuksEncryptor"
         control_location = "front-end"
         name = data_utils.rand_name("volume-type")
-        body = self.volume_types_client.create_volume_type(
+        body = self.admin_volume_types_client.create_volume_type(
             name=name)['volume_type']
-        self.addCleanup(self.volume_types_client.delete_volume_type,
+        self.addCleanup(self.admin_volume_types_client.delete_volume_type,
                         body['id'])
 
         # Create encryption type
-        encryption_type = self.volume_types_client.create_encryption_type(
-            body['id'], provider=provider,
-            control_location=control_location)['encryption']
+        encryption_type = \
+            self.admin_volume_types_client.create_encryption_type(
+                body['id'], provider=provider,
+                control_location=control_location)['encryption']
         self.assertIn('volume_type_id', encryption_type)
         self.assertEqual(provider, encryption_type['provider'],
                          "The created encryption_type provider is not equal "
@@ -146,7 +148,7 @@
 
         # Get encryption type
         fetched_encryption_type = (
-            self.volume_types_client.show_encryption_type(
+            self.admin_volume_types_client.show_encryption_type(
                 encryption_type['volume_type_id']))
         self.assertEqual(provider,
                          fetched_encryption_type['provider'],
@@ -158,13 +160,13 @@
                          'different from the created encryption_type')
 
         # Delete encryption type
-        self.volume_types_client.delete_encryption_type(
+        self.admin_volume_types_client.delete_encryption_type(
             encryption_type['volume_type_id'])
         resource = {"id": encryption_type['volume_type_id'],
                     "type": "encryption-type"}
-        self.volume_types_client.wait_for_resource_deletion(resource)
+        self.admin_volume_types_client.wait_for_resource_deletion(resource)
         deleted_encryption_type = (
-            self.volume_types_client.show_encryption_type(
+            self.admin_volume_types_client.show_encryption_type(
                 encryption_type['volume_type_id']))
         self.assertEmpty(deleted_encryption_type)
 
diff --git a/tempest/api/volume/admin/test_volume_types_extra_specs.py b/tempest/api/volume/admin/test_volume_types_extra_specs.py
index 502cd86..a45bee0 100644
--- a/tempest/api/volume/admin/test_volume_types_extra_specs.py
+++ b/tempest/api/volume/admin/test_volume_types_extra_specs.py
@@ -24,23 +24,24 @@
     def resource_setup(cls):
         super(VolumeTypesExtraSpecsV2Test, cls).resource_setup()
         vol_type_name = data_utils.rand_name('Volume-type')
-        cls.volume_type = cls.volume_types_client.create_volume_type(
-            name=vol_type_name)['volume_type']
+        cls.volume_type = \
+            cls.admin_volume_types_client.create_volume_type(
+                name=vol_type_name)['volume_type']
 
     @classmethod
     def resource_cleanup(cls):
-        cls.volume_types_client.delete_volume_type(cls.volume_type['id'])
+        cls.admin_volume_types_client.delete_volume_type(cls.volume_type['id'])
         super(VolumeTypesExtraSpecsV2Test, cls).resource_cleanup()
 
     @test.idempotent_id('b42923e9-0452-4945-be5b-d362ae533e60')
     def test_volume_type_extra_specs_list(self):
         # List Volume types extra specs.
         extra_specs = {"spec1": "val1"}
-        body = self.volume_types_client.create_volume_type_extra_specs(
+        body = self.admin_volume_types_client.create_volume_type_extra_specs(
             self.volume_type['id'], extra_specs)['extra_specs']
         self.assertEqual(extra_specs, body,
                          "Volume type extra spec incorrectly created")
-        body = self.volume_types_client.list_volume_types_extra_specs(
+        body = self.admin_volume_types_client.list_volume_types_extra_specs(
             self.volume_type['id'])['extra_specs']
         self.assertIsInstance(body, dict)
         self.assertIn('spec1', body)
@@ -49,13 +50,13 @@
     def test_volume_type_extra_specs_update(self):
         # Update volume type extra specs
         extra_specs = {"spec2": "val1"}
-        body = self.volume_types_client.create_volume_type_extra_specs(
+        body = self.admin_volume_types_client.create_volume_type_extra_specs(
             self.volume_type['id'], extra_specs)['extra_specs']
         self.assertEqual(extra_specs, body,
                          "Volume type extra spec incorrectly created")
 
         extra_spec = {"spec2": "val2"}
-        body = self.volume_types_client.update_volume_type_extra_specs(
+        body = self.admin_volume_types_client.update_volume_type_extra_specs(
             self.volume_type['id'],
             extra_spec.keys()[0],
             extra_spec)
@@ -67,19 +68,19 @@
     def test_volume_type_extra_spec_create_get_delete(self):
         # Create/Get/Delete volume type extra spec.
         extra_specs = {"spec3": "val1"}
-        body = self.volume_types_client.create_volume_type_extra_specs(
+        body = self.admin_volume_types_client.create_volume_type_extra_specs(
             self.volume_type['id'],
             extra_specs)['extra_specs']
         self.assertEqual(extra_specs, body,
                          "Volume type extra spec incorrectly created")
 
-        self.volume_types_client.show_volume_type_extra_specs(
+        self.admin_volume_types_client.show_volume_type_extra_specs(
             self.volume_type['id'],
             extra_specs.keys()[0])
         self.assertEqual(extra_specs, body,
                          "Volume type extra spec incorrectly fetched")
 
-        self.volume_types_client.delete_volume_type_extra_specs(
+        self.admin_volume_types_client.delete_volume_type_extra_specs(
             self.volume_type['id'],
             extra_specs.keys()[0])
 
diff --git a/tempest/api/volume/admin/test_volume_types_extra_specs_negative.py b/tempest/api/volume/admin/test_volume_types_extra_specs_negative.py
index f3e52e9..6983974 100644
--- a/tempest/api/volume/admin/test_volume_types_extra_specs_negative.py
+++ b/tempest/api/volume/admin/test_volume_types_extra_specs_negative.py
@@ -26,13 +26,13 @@
         super(ExtraSpecsNegativeV2Test, cls).resource_setup()
         vol_type_name = data_utils.rand_name('Volume-type')
         cls.extra_specs = {"spec1": "val1"}
-        cls.volume_type = cls.volume_types_client.create_volume_type(
+        cls.volume_type = cls.admin_volume_types_client.create_volume_type(
             name=vol_type_name,
             extra_specs=cls.extra_specs)['volume_type']
 
     @classmethod
     def resource_cleanup(cls):
-        cls.volume_types_client.delete_volume_type(cls.volume_type['id'])
+        cls.admin_volume_types_client.delete_volume_type(cls.volume_type['id'])
         super(ExtraSpecsNegativeV2Test, cls).resource_cleanup()
 
     @test.idempotent_id('08961d20-5cbb-4910-ac0f-89ad6dbb2da1')
@@ -41,7 +41,7 @@
         extra_spec = {"spec1": "val2"}
         self.assertRaises(
             lib_exc.BadRequest,
-            self.volume_types_client.update_volume_type_extra_specs,
+            self.admin_volume_types_client.update_volume_type_extra_specs,
             self.volume_type['id'], extra_spec.keys()[0], None)
 
     @test.idempotent_id('25e5a0ee-89b3-4c53-8310-236f76c75365')
@@ -50,7 +50,7 @@
         extra_spec = {"spec1": "val2"}
         self.assertRaises(
             lib_exc.BadRequest,
-            self.volume_types_client.update_volume_type_extra_specs,
+            self.admin_volume_types_client.update_volume_type_extra_specs,
             self.volume_type['id'], data_utils.rand_uuid(),
             extra_spec)
 
@@ -60,7 +60,7 @@
         extra_spec = {"spec1": "val2"}
         self.assertRaises(
             lib_exc.BadRequest,
-            self.volume_types_client.update_volume_type_extra_specs,
+            self.admin_volume_types_client.update_volume_type_extra_specs,
             self.volume_type['id'], None, extra_spec)
 
     @test.idempotent_id('a77dfda2-9100-448e-9076-ed1711f4bdfc')
@@ -70,7 +70,7 @@
         extra_spec = {"spec1": "val2", "spec2": "val1"}
         self.assertRaises(
             lib_exc.BadRequest,
-            self.volume_types_client.update_volume_type_extra_specs,
+            self.admin_volume_types_client.update_volume_type_extra_specs,
             self.volume_type['id'], extra_spec.keys()[0],
             extra_spec)
 
@@ -81,7 +81,7 @@
         extra_specs = {"spec2": "val1"}
         self.assertRaises(
             lib_exc.NotFound,
-            self.volume_types_client.create_volume_type_extra_specs,
+            self.admin_volume_types_client.create_volume_type_extra_specs,
             data_utils.rand_uuid(), extra_specs)
 
     @test.idempotent_id('c821bdc8-43a4-4bf4-86c8-82f3858d5f7d')
@@ -89,7 +89,7 @@
         # Should not create volume type extra spec for none POST body.
         self.assertRaises(
             lib_exc.BadRequest,
-            self.volume_types_client.create_volume_type_extra_specs,
+            self.admin_volume_types_client.create_volume_type_extra_specs,
             self.volume_type['id'], None)
 
     @test.idempotent_id('bc772c71-1ed4-4716-b945-8b5ed0f15e87')
@@ -97,7 +97,7 @@
         # Should not create volume type extra spec for invalid POST body.
         self.assertRaises(
             lib_exc.BadRequest,
-            self.volume_types_client.create_volume_type_extra_specs,
+            self.admin_volume_types_client.create_volume_type_extra_specs,
             self.volume_type['id'], extra_specs=['invalid'])
 
     @test.idempotent_id('031cda8b-7d23-4246-8bf6-bbe73fd67074')
@@ -107,7 +107,7 @@
         extra_specs = {"spec1": "val1"}
         self.assertRaises(
             lib_exc.NotFound,
-            self.volume_types_client.delete_volume_type_extra_specs,
+            self.admin_volume_types_client.delete_volume_type_extra_specs,
             data_utils.rand_uuid(), extra_specs.keys()[0])
 
     @test.idempotent_id('dee5cf0c-cdd6-4353-b70c-e847050d71fb')
@@ -115,7 +115,7 @@
         # Should not list volume type extra spec for nonexistent type id.
         self.assertRaises(
             lib_exc.NotFound,
-            self.volume_types_client.list_volume_types_extra_specs,
+            self.admin_volume_types_client.list_volume_types_extra_specs,
             data_utils.rand_uuid())
 
     @test.idempotent_id('9f402cbd-1838-4eb4-9554-126a6b1908c9')
@@ -124,7 +124,7 @@
         extra_specs = {"spec1": "val1"}
         self.assertRaises(
             lib_exc.NotFound,
-            self.volume_types_client.show_volume_type_extra_specs,
+            self.admin_volume_types_client.show_volume_type_extra_specs,
             data_utils.rand_uuid(), extra_specs.keys()[0])
 
     @test.idempotent_id('c881797d-12ff-4f1a-b09d-9f6212159753')
@@ -133,7 +133,7 @@
             # id.
         self.assertRaises(
             lib_exc.NotFound,
-            self.volume_types_client.show_volume_type_extra_specs,
+            self.admin_volume_types_client.show_volume_type_extra_specs,
             self.volume_type['id'], data_utils.rand_uuid())
 
 
diff --git a/tempest/api/volume/admin/test_volume_types_negative.py b/tempest/api/volume/admin/test_volume_types_negative.py
index aff5466..857e7d2 100644
--- a/tempest/api/volume/admin/test_volume_types_negative.py
+++ b/tempest/api/volume/admin/test_volume_types_negative.py
@@ -33,21 +33,22 @@
     @test.idempotent_id('878b4e57-faa2-4659-b0d1-ce740a06ae81')
     def test_create_with_empty_name(self):
         # Should not be able to create volume type with an empty name.
-        self.assertRaises(lib_exc.BadRequest,
-                          self.volume_types_client.create_volume_type, name='')
+        self.assertRaises(
+            lib_exc.BadRequest,
+            self.admin_volume_types_client.create_volume_type, name='')
 
     @test.idempotent_id('994610d6-0476-4018-a644-a2602ef5d4aa')
     def test_get_nonexistent_type_id(self):
         # Should not be able to get volume type with nonexistent type id.
         self.assertRaises(lib_exc.NotFound,
-                          self.volume_types_client.show_volume_type,
+                          self.admin_volume_types_client.show_volume_type,
                           data_utils.rand_uuid())
 
     @test.idempotent_id('6b3926d2-7d73-4896-bc3d-e42dfd11a9f6')
     def test_delete_nonexistent_type_id(self):
         # Should not be able to delete volume type with nonexistent type id.
         self.assertRaises(lib_exc.NotFound,
-                          self.volume_types_client.delete_volume_type,
+                          self.admin_volume_types_client.delete_volume_type,
                           data_utils.rand_uuid())
 
 
diff --git a/tempest/api/volume/admin/test_volumes_backup.py b/tempest/api/volume/admin/test_volumes_backup.py
index 1289297..66bab51 100644
--- a/tempest/api/volume/admin/test_volumes_backup.py
+++ b/tempest/api/volume/admin/test_volumes_backup.py
@@ -42,8 +42,8 @@
         cls.volume = cls.create_volume()
 
     def _delete_backup(self, backup_id):
-        self.backups_adm_client.delete_backup(backup_id)
-        self.backups_adm_client.wait_for_backup_deletion(backup_id)
+        self.admin_backups_client.delete_backup(backup_id)
+        self.admin_backups_client.wait_for_backup_deletion(backup_id)
 
     def _decode_url(self, backup_url):
         return json.loads(base64.decodestring(backup_url))
@@ -63,36 +63,37 @@
     def test_volume_backup_create_get_detailed_list_restore_delete(self):
         # Create backup
         backup_name = data_utils.rand_name('Backup')
-        create_backup = self.backups_adm_client.create_backup
+        create_backup = self.admin_backups_client.create_backup
         backup = create_backup(volume_id=self.volume['id'],
                                name=backup_name)['backup']
-        self.addCleanup(self.backups_adm_client.delete_backup,
+        self.addCleanup(self.admin_backups_client.delete_backup,
                         backup['id'])
         self.assertEqual(backup_name, backup['name'])
         waiters.wait_for_volume_status(self.admin_volume_client,
                                        self.volume['id'], 'available')
-        self.backups_adm_client.wait_for_backup_status(backup['id'],
-                                                       'available')
+        self.admin_backups_client.wait_for_backup_status(backup['id'],
+                                                         'available')
 
         # Get a given backup
-        backup = self.backups_adm_client.show_backup(backup['id'])['backup']
+        backup = self.admin_backups_client.show_backup(backup['id'])['backup']
         self.assertEqual(backup_name, backup['name'])
 
         # Get all backups with detail
-        backups = self.backups_adm_client.list_backups(detail=True)['backups']
+        backups = self.admin_backups_client.list_backups(
+            detail=True)['backups']
         self.assertIn((backup['name'], backup['id']),
                       [(m['name'], m['id']) for m in backups])
 
         # Restore backup
-        restore = self.backups_adm_client.restore_backup(
+        restore = self.admin_backups_client.restore_backup(
             backup['id'])['restore']
 
         # Delete backup
         self.addCleanup(self.admin_volume_client.delete_volume,
                         restore['volume_id'])
         self.assertEqual(backup['id'], restore['backup_id'])
-        self.backups_adm_client.wait_for_backup_status(backup['id'],
-                                                       'available')
+        self.admin_backups_client.wait_for_backup_status(backup['id'],
+                                                         'available')
         waiters.wait_for_volume_status(self.admin_volume_client,
                                        restore['volume_id'], 'available')
 
@@ -105,15 +106,15 @@
         """
         # Create backup
         backup_name = data_utils.rand_name('Backup')
-        backup = (self.backups_adm_client.create_backup(
+        backup = (self.admin_backups_client.create_backup(
             volume_id=self.volume['id'], name=backup_name)['backup'])
         self.addCleanup(self._delete_backup, backup['id'])
         self.assertEqual(backup_name, backup['name'])
-        self.backups_adm_client.wait_for_backup_status(backup['id'],
-                                                       'available')
+        self.admin_backups_client.wait_for_backup_status(backup['id'],
+                                                         'available')
 
         # Export Backup
-        export_backup = (self.backups_adm_client.export_backup(backup['id'])
+        export_backup = (self.admin_backups_client.export_backup(backup['id'])
                          ['backup-record'])
         self.assertIn('backup_service', export_backup)
         self.assertIn('backup_url', export_backup)
@@ -132,7 +133,7 @@
             export_backup['backup_url'], {'id': new_id})
 
         # Import Backup
-        import_backup = self.backups_adm_client.import_backup(
+        import_backup = self.admin_backups_client.import_backup(
             backup_service=export_backup['backup_service'],
             backup_url=new_url)['backup']
 
@@ -142,15 +143,16 @@
         self.addCleanup(self._delete_backup, new_id)
         self.assertIn("id", import_backup)
         self.assertEqual(new_id, import_backup['id'])
-        self.backups_adm_client.wait_for_backup_status(import_backup['id'],
-                                                       'available')
+        self.admin_backups_client.wait_for_backup_status(import_backup['id'],
+                                                         'available')
 
         # Verify Import Backup
-        backups = self.backups_adm_client.list_backups(detail=True)['backups']
+        backups = self.admin_backups_client.list_backups(
+            detail=True)['backups']
         self.assertIn(new_id, [b['id'] for b in backups])
 
         # Restore backup
-        restore = self.backups_adm_client.restore_backup(
+        restore = self.admin_backups_client.restore_backup(
             backup['id'])['restore']
         self.addCleanup(self.admin_volume_client.delete_volume,
                         restore['volume_id'])
@@ -161,8 +163,8 @@
         # Verify if restored volume is there in volume list
         volumes = self.admin_volume_client.list_volumes()['volumes']
         self.assertIn(restore['volume_id'], [v['id'] for v in volumes])
-        self.backups_adm_client.wait_for_backup_status(import_backup['id'],
-                                                       'available')
+        self.admin_backups_client.wait_for_backup_status(import_backup['id'],
+                                                         'available')
 
 
 class VolumesBackupsV1Test(VolumesBackupsV2Test):
diff --git a/tempest/api/volume/base.py b/tempest/api/volume/base.py
index 6116d72..cd21424 100644
--- a/tempest/api/volume/base.py
+++ b/tempest/api/volume/base.py
@@ -179,25 +179,25 @@
         super(BaseVolumeAdminTest, cls).setup_clients()
 
         if cls._api_version == 1:
-            cls.volume_qos_client = cls.os_adm.volume_qos_client
+            cls.admin_volume_qos_client = cls.os_adm.volume_qos_client
             cls.admin_volume_services_client = \
                 cls.os_adm.volume_services_client
-            cls.volume_types_client = cls.os_adm.volume_types_client
+            cls.admin_volume_types_client = cls.os_adm.volume_types_client
             cls.admin_volume_client = cls.os_adm.volumes_client
-            cls.hosts_client = cls.os_adm.volume_hosts_client
+            cls.admin_hosts_client = cls.os_adm.volume_hosts_client
             cls.admin_snapshots_client = cls.os_adm.snapshots_client
-            cls.backups_adm_client = cls.os_adm.backups_client
-            cls.quotas_client = cls.os_adm.volume_quotas_client
+            cls.admin_backups_client = cls.os_adm.backups_client
+            cls.admin_quotas_client = cls.os_adm.volume_quotas_client
         elif cls._api_version == 2:
-            cls.volume_qos_client = cls.os_adm.volume_qos_v2_client
+            cls.admin_volume_qos_client = cls.os_adm.volume_qos_v2_client
             cls.admin_volume_services_client = \
                 cls.os_adm.volume_services_v2_client
-            cls.volume_types_client = cls.os_adm.volume_types_v2_client
+            cls.admin_volume_types_client = cls.os_adm.volume_types_v2_client
             cls.admin_volume_client = cls.os_adm.volumes_v2_client
-            cls.hosts_client = cls.os_adm.volume_hosts_v2_client
+            cls.admin_hosts_client = cls.os_adm.volume_hosts_v2_client
             cls.admin_snapshots_client = cls.os_adm.snapshots_v2_client
-            cls.backups_adm_client = cls.os_adm.backups_v2_client
-            cls.quotas_client = cls.os_adm.volume_quotas_v2_client
+            cls.admin_backups_client = cls.os_adm.backups_v2_client
+            cls.admin_quotas_client = cls.os_adm.volume_quotas_v2_client
 
     @classmethod
     def resource_setup(cls):
@@ -215,7 +215,7 @@
         """create a test Qos-Specs."""
         name = name or data_utils.rand_name(cls.__name__ + '-QoS')
         consumer = consumer or 'front-end'
-        qos_specs = cls.volume_qos_client.create_qos(
+        qos_specs = cls.admin_volume_qos_client.create_qos(
             name=name, consumer=consumer, **kwargs)['qos_specs']
         cls.qos_specs.append(qos_specs['id'])
         return qos_specs
@@ -224,8 +224,8 @@
     def clear_qos_specs(cls):
         for qos_id in cls.qos_specs:
             test_utils.call_and_ignore_notfound_exc(
-                cls.volume_qos_client.delete_qos, qos_id)
+                cls.admin_volume_qos_client.delete_qos, qos_id)
 
         for qos_id in cls.qos_specs:
             test_utils.call_and_ignore_notfound_exc(
-                cls.volume_qos_client.wait_for_resource_deletion, qos_id)
+                cls.admin_volume_qos_client.wait_for_resource_deletion, qos_id)
diff --git a/tempest/cmd/account_generator.py b/tempest/cmd/account_generator.py
index 249261b..0a5a41b 100755
--- a/tempest/cmd/account_generator.py
+++ b/tempest/cmd/account_generator.py
@@ -38,17 +38,17 @@
 of your cloud to operate properly. The corresponding info can be given either
 through CLI options or environment variables.
 
-You're probably familiar with these, but just to remind::
+You're probably familiar with these, but just to remind:
 
-    +----------+---------------------------+----------------------+
-    | Param    | CLI                       | Environment Variable |
-    +----------+---------------------------+----------------------+
-    | Username | --os-username             | OS_USERNAME          |
-    | Password | --os-password             | OS_PASSWORD          |
-    | Project  | --os-project-name         | OS_PROJECT_NAME      |
-    | Tenant   | --os-tenant-name (depr.)  | OS_TENANT_NAME       |
-    | Domain   | --os-domain-name          | OS_DOMAIN_NAME       |
-    +----------+---------------------------+----------------------+
+======== ======================== ====================
+Param    CLI                      Environment Variable
+======== ======================== ====================
+Username --os-username            OS_USERNAME
+Password --os-password            OS_PASSWORD
+Project  --os-project-name        OS_PROJECT_NAME
+Tenant   --os-tenant-name (depr.) OS_TENANT_NAME
+Domain   --os-domain-name         OS_DOMAIN_NAME
+======== ======================== ====================
 
 Optional Arguments
 ------------------
@@ -277,12 +277,11 @@
 
     def take_action(self, parsed_args):
         try:
-            return main(parsed_args)
+            main(parsed_args)
         except Exception:
             LOG.exception("Failure generating test accounts.")
             traceback.print_exc()
             raise
-        return 0
 
     def get_description(self):
         return DESCRIPTION
diff --git a/tempest/cmd/cleanup.py b/tempest/cmd/cleanup.py
index caba4b5..e8e691e 100644
--- a/tempest/cmd/cleanup.py
+++ b/tempest/cmd/cleanup.py
@@ -83,7 +83,6 @@
             LOG.exception("Failure during cleanup")
             traceback.print_exc()
             raise
-        return 0
 
     def init(self, parsed_args):
         cleanup_service.init_conf()
diff --git a/tempest/cmd/init.py b/tempest/cmd/init.py
index 633b9e9..77d62d3 100644
--- a/tempest/cmd/init.py
+++ b/tempest/cmd/init.py
@@ -21,6 +21,8 @@
 from oslo_log import log as logging
 from six import moves
 
+from tempest.cmd.workspace import WorkspaceManager
+
 LOG = logging.getLogger(__name__)
 
 TESTR_CONF = """[DEFAULT]
@@ -89,6 +91,10 @@
                             action='store_true', dest='show_global_dir',
                             help="Print the global config dir location, "
                                  "then exit")
+        parser.add_argument('--name', help="The workspace name", default=None)
+        parser.add_argument('--workspace-path', default=None,
+                            help="The path to the workspace file, the default "
+                                 "is ~/.tempest/workspace")
         return parser
 
     def generate_testr_conf(self, local_path):
@@ -114,15 +120,22 @@
             config_parse.write(conf_file)
 
     def copy_config(self, etc_dir, config_dir):
-        shutil.copytree(config_dir, etc_dir)
+        if os.path.isdir(config_dir):
+            shutil.copytree(config_dir, etc_dir)
+        else:
+            LOG.warning("Global config dir %s can't be found" % config_dir)
 
     def generate_sample_config(self, local_dir, config_dir):
-        conf_generator = os.path.join(config_dir,
-                                      'config-generator.tempest.conf')
+        if os.path.isdir(config_dir):
+            conf_generator = os.path.join(config_dir,
+                                          'config-generator.tempest.conf')
 
-        subprocess.call(['oslo-config-generator', '--config-file',
-                         conf_generator],
-                        cwd=local_dir)
+            subprocess.call(['oslo-config-generator', '--config-file',
+                             conf_generator],
+                            cwd=local_dir)
+        else:
+            LOG.warning("Skipping sample config generation because global "
+                        "config dir %s can't be found" % config_dir)
 
     def create_working_dir(self, local_dir, config_dir):
         # Create local dir if missing
@@ -159,6 +172,10 @@
             subprocess.call(['testr', 'init'], cwd=local_dir)
 
     def take_action(self, parsed_args):
+        workspace_manager = WorkspaceManager(parsed_args.workspace_path)
+        name = parsed_args.name or parsed_args.dir.split(os.path.sep)[-1]
+        workspace_manager.register_new_workspace(
+            name, parsed_args.dir, init=True)
         config_dir = parsed_args.config_dir or get_tempest_default_config_dir()
         if parsed_args.show_global_dir:
             print("Global config dir is located at: %s" % config_dir)
diff --git a/tempest/cmd/list_plugins.py b/tempest/cmd/list_plugins.py
index 1f1ff1a..5f6b3e6 100644
--- a/tempest/cmd/list_plugins.py
+++ b/tempest/cmd/list_plugins.py
@@ -30,7 +30,6 @@
 class TempestListPlugins(command.Command):
     def take_action(self, parsed_args):
         self._list_plugins()
-        return 0
 
     def get_description(self):
         return 'List all tempest plugins'
diff --git a/tempest/cmd/run.py b/tempest/cmd/run.py
new file mode 100644
index 0000000..b4b7ebb
--- /dev/null
+++ b/tempest/cmd/run.py
@@ -0,0 +1,181 @@
+# 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.
+
+"""
+Runs tempest tests
+
+This command is used for running the tempest tests
+
+Test Selection
+==============
+Tempest run has several options:
+
+ * **--regex/-r**: This is a selection regex like what testr uses. It will run
+                   any tests that match on re.match() with the regex
+ * **--smoke**: Run all the tests tagged as smoke
+
+You can also use the **--list-tests** option in conjunction with selection
+arguments to list which tests will be run.
+
+Test Execution
+==============
+There are several options to control how the tests are executed. By default
+tempest will run in parallel with a worker for each CPU present on the machine.
+If you want to adjust the number of workers use the **--concurrency** option
+and if you want to run tests serially use **--serial**
+
+Test Output
+===========
+By default tempest run's output to STDOUT will be generated using the
+subunit-trace output filter. But, if you would prefer a subunit v2 stream be
+output to STDOUT use the **--subunit** flag
+
+"""
+
+import io
+import os
+import sys
+import threading
+
+from cliff import command
+from os_testr import subunit_trace
+from oslo_log import log as logging
+from testrepository.commands import run_argv
+
+from tempest import config
+
+
+LOG = logging.getLogger(__name__)
+CONF = config.CONF
+
+
+class TempestRun(command.Command):
+
+    def _set_env(self):
+        # NOTE(mtreinish): This is needed so that testr doesn't gobble up any
+        # stacktraces on failure.
+        if 'TESTR_PDB' in os.environ:
+            return
+        else:
+            os.environ["TESTR_PDB"] = ""
+
+    def take_action(self, parsed_args):
+        self._set_env()
+        # Local exceution mode
+        if os.path.isfile('.testr.conf'):
+            # If you're running in local execution mode and there is not a
+            # testrepository dir create one
+            if not os.path.isdir('.testrepository'):
+                returncode = run_argv(['testr', 'init'], sys.stdin, sys.stdout,
+                                      sys.stderr)
+            if returncode:
+                sys.exit(returncode)
+        else:
+            print("No .testr.conf file was found for local exceution")
+            sys.exit(2)
+
+        regex = self._build_regex(parsed_args)
+        if parsed_args.list_tests:
+            argv = ['tempest', 'list-tests', regex]
+            returncode = run_argv(argv, sys.stdin, sys.stdout, sys.stderr)
+        else:
+            options = self._build_options(parsed_args)
+            returncode = self._run(regex, options)
+        sys.exit(returncode)
+
+    def get_description(self):
+        return 'Run tempest'
+
+    def get_parser(self, prog_name):
+        parser = super(TempestRun, self).get_parser(prog_name)
+        parser = self._add_args(parser)
+        return parser
+
+    def _add_args(self, parser):
+        # test selection args
+        regex = parser.add_mutually_exclusive_group()
+        regex.add_argument('--smoke', action='store_true',
+                           help="Run the smoke tests only")
+        regex.add_argument('--regex', '-r', default='',
+                           help='A normal testr selection regex used to '
+                                'specify a subset of tests to run')
+        # list only args
+        parser.add_argument('--list-tests', '-l', action='store_true',
+                            help='List tests',
+                            default=False)
+        # exectution args
+        parser.add_argument('--concurrency', '-w',
+                            help="The number of workers to use, defaults to "
+                                 "the number of cpus")
+        parallel = parser.add_mutually_exclusive_group()
+        parallel.add_argument('--parallel', dest='parallel',
+                              action='store_true',
+                              help='Run tests in parallel (this is the'
+                                   ' default)')
+        parallel.add_argument('--serial', dest='parallel',
+                              action='store_false',
+                              help='Run tests serially')
+        # output args
+        parser.add_argument("--subunit", action='store_true',
+                            help='Enable subunit v2 output')
+
+        parser.set_defaults(parallel=True)
+        return parser
+
+    def _build_regex(self, parsed_args):
+        regex = ''
+        if parsed_args.smoke:
+            regex = 'smoke'
+        elif parsed_args.regex:
+            regex = parsed_args.regex
+        return regex
+
+    def _build_options(self, parsed_args):
+        options = []
+        if parsed_args.subunit:
+            options.append("--subunit")
+        if parsed_args.parallel:
+            options.append("--parallel")
+        if parsed_args.concurrency:
+            options.append("--concurrency=%s" % parsed_args.concurrency)
+        return options
+
+    def _run(self, regex, options):
+        returncode = 0
+        argv = ['tempest', 'run', regex] + options
+        if '--subunit' in options:
+            returncode = run_argv(argv, sys.stdin, sys.stdout, sys.stderr)
+        else:
+            argv.append('--subunit')
+            stdin = io.StringIO()
+            stdout_r, stdout_w = os.pipe()
+            subunit_w = os.fdopen(stdout_w, 'wt')
+            subunit_r = os.fdopen(stdout_r)
+            returncodes = {}
+
+            def run_argv_thread():
+                returncodes['testr'] = run_argv(argv, stdin, subunit_w,
+                                                sys.stderr)
+                subunit_w.close()
+
+            run_thread = threading.Thread(target=run_argv_thread)
+            run_thread.start()
+            returncodes['subunit-trace'] = subunit_trace.trace(subunit_r,
+                                                               sys.stdout)
+            run_thread.join()
+            subunit_r.close()
+            # python version of pipefail
+            if returncodes['testr']:
+                returncode = returncodes['testr']
+            elif returncodes['subunit-trace']:
+                returncode = returncodes['subunit-trace']
+        return returncode
diff --git a/tempest/cmd/run_stress.py b/tempest/cmd/run_stress.py
index 9c8552f..7502c23 100755
--- a/tempest/cmd/run_stress.py
+++ b/tempest/cmd/run_stress.py
@@ -97,7 +97,6 @@
             LOG.exception("Failure in the stress test framework")
             traceback.print_exc()
             raise
-        return 0
 
     def get_description(self):
         return 'Run tempest stress tests'
diff --git a/tempest/cmd/verify_tempest_config.py b/tempest/cmd/verify_tempest_config.py
index f558740..4b12ecb 100644
--- a/tempest/cmd/verify_tempest_config.py
+++ b/tempest/cmd/verify_tempest_config.py
@@ -418,12 +418,11 @@
 
     def take_action(self, parsed_args):
         try:
-            return main(parsed_args)
+            main(parsed_args)
         except Exception:
             LOG.exception("Failure verifying configuration.")
             traceback.print_exc()
             raise
-        return 0
 
 if __name__ == "__main__":
     main()
diff --git a/tempest/cmd/workspace.py b/tempest/cmd/workspace.py
new file mode 100644
index 0000000..cc82284
--- /dev/null
+++ b/tempest/cmd/workspace.py
@@ -0,0 +1,218 @@
+# Copyright 2016 Rackspace
+#
+# 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.
+
+"""
+Manages Tempest workspaces
+
+This command is used for managing tempest workspaces
+
+Commands
+========
+
+list
+----
+Outputs the name and path of all known tempest workspaces
+
+register
+--------
+Registers a new tempest workspace via a given --name and --path
+
+rename
+------
+Renames a tempest workspace from --old-name to --new-name
+
+move
+----
+Changes the path of a given tempest workspace --name to --path
+
+remove
+------
+Deletes the entry for a given tempest workspace --name
+
+General Options
+===============
+
+ **--workspace_path**: Allows the user to specify a different location for the
+                       workspace.yaml file containing the workspace definitions
+                       instead of ~/.tempest/workspace.yaml
+"""
+
+import os
+import sys
+
+from cliff import command
+from oslo_concurrency import lockutils
+from oslo_log import log as logging
+import prettytable
+import yaml
+
+from tempest import config
+
+LOG = logging.getLogger(__name__)
+CONF = config.CONF
+
+
+class WorkspaceManager(object):
+    def __init__(self, path=None):
+        lockutils.get_lock_path(CONF)
+        self.path = path or os.path.join(
+            os.path.expanduser("~"), ".tempest", "workspace.yaml")
+        if not os.path.isdir(os.path.dirname(self.path)):
+            os.makedirs(self.path.rsplit(os.path.sep, 1)[0])
+        self.workspaces = {}
+
+    @lockutils.synchronized('workspaces', external=True)
+    def get_workspace(self, name):
+        """Returns the workspace that has the given name"""
+        self._populate()
+        return self.workspaces.get(name)
+
+    @lockutils.synchronized('workspaces', external=True)
+    def rename_workspace(self, old_name, new_name):
+        self._populate()
+        self._name_exists(old_name)
+        self._workspace_name_exists(new_name)
+        self.workspaces[new_name] = self.workspaces.pop(old_name)
+        self._write_file()
+
+    @lockutils.synchronized('workspaces', external=True)
+    def move_workspace(self, name, path):
+        self._populate()
+        path = os.path.abspath(os.path.expanduser(path))
+        self._name_exists(name)
+        self._validate_path(path)
+        self.workspaces[name] = path
+        self._write_file()
+
+    def _name_exists(self, name):
+        if name not in self.workspaces:
+            print("A workspace was not found with name: {0}".format(name))
+            sys.exit(1)
+
+    @lockutils.synchronized('workspaces', external=True)
+    def remove_workspace(self, name):
+        self._populate()
+        self._name_exists(name)
+        self.workspaces.pop(name)
+        self._write_file()
+
+    @lockutils.synchronized('workspaces', external=True)
+    def list_workspaces(self):
+        self._populate()
+        self._validate_workspaces()
+        return self.workspaces
+
+    def _workspace_name_exists(self, name):
+        if name in self.workspaces:
+            print("A workspace already exists with name: {0}.".format(
+                name))
+            sys.exit(1)
+
+    def _validate_path(self, path):
+        if not os.path.exists(path):
+            print("Path does not exist.")
+            sys.exit(1)
+
+    @lockutils.synchronized('workspaces', external=True)
+    def register_new_workspace(self, name, path, init=False):
+        """Adds the new workspace and writes out the new workspace config"""
+        self._populate()
+        path = os.path.abspath(os.path.expanduser(path))
+        # This only happens when register is called from outside of init
+        if not init:
+            self._validate_path(path)
+        self._workspace_name_exists(name)
+        self.workspaces[name] = path
+        self._write_file()
+
+    def _validate_workspaces(self):
+        if self.workspaces is not None:
+            self.workspaces = {n: p for n, p in self.workspaces.items()
+                               if os.path.exists(p)}
+            self._write_file()
+
+    def _write_file(self):
+        with open(self.path, 'w') as f:
+            f.write(yaml.dump(self.workspaces))
+
+    def _populate(self):
+        if not os.path.isfile(self.path):
+            return
+        with open(self.path, 'r') as f:
+            self.workspaces = yaml.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 get_description(self):
+        return 'Tempest workspace actions'
+
+    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')
+        list_parser.set_defaults(list=True)
+
+        register_parser = subparsers.add_parser('register')
+        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')
+        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')
+        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')
+        remove_parser.add_argument('--name', required=True)
+        remove_parser.set_defaults(remove=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])
+
+        print(output)
diff --git a/tempest/common/credentials_factory.py b/tempest/common/credentials_factory.py
index 4873fcf..c22afc1 100644
--- a/tempest/common/credentials_factory.py
+++ b/tempest/common/credentials_factory.py
@@ -193,17 +193,21 @@
 
 
 # Wrapper around auth.get_credentials to use the configured identity version
-# is none is specified
+# if none is specified
 def get_credentials(fill_in=True, identity_version=None, **kwargs):
     params = dict(DEFAULT_PARAMS, **kwargs)
     identity_version = identity_version or CONF.identity.auth_version
     # In case of "v3" add the domain from config if not specified
+    # To honour the "default_credentials_domain_name", if not domain
+    # field is specified at all, add it the credential dict.
     if identity_version == 'v3':
         domain_fields = set(x for x in auth.KeystoneV3Credentials.ATTRIBUTES
                             if 'domain' in x)
         if not domain_fields.intersection(kwargs.keys()):
             domain_name = CONF.auth.default_credentials_domain_name
-            params['user_domain_name'] = domain_name
+            # NOTE(andreaf) Setting domain_name implicitly sets user and
+            # project domain names, if they are None
+            params['domain_name'] = domain_name
 
         auth_url = CONF.identity.uri_v3
     else:
diff --git a/tempest/common/dynamic_creds.py b/tempest/common/dynamic_creds.py
index 3a3e3c2..0b92c15 100644
--- a/tempest/common/dynamic_creds.py
+++ b/tempest/common/dynamic_creds.py
@@ -96,8 +96,15 @@
                     os.networks_client, os.routers_client, os.subnets_client,
                     os.ports_client, os.security_groups_client)
         else:
-            return (os.identity_v3_client, os.projects_client,
-                    os.users_v3_client, os.roles_v3_client, os.domains_client,
+            # We use a dedicated client manager for identity client in case we
+            # need a different token scope for them.
+            scope = 'domain' if CONF.identity.admin_domain_scope else 'project'
+            identity_os = clients.Manager(self.default_admin_creds,
+                                          scope=scope)
+            return (identity_os.identity_v3_client,
+                    identity_os.projects_client,
+                    identity_os.users_v3_client, identity_os.roles_v3_client,
+                    identity_os.domains_client,
                     os.networks_client, os.routers_client,
                     os.subnets_client, os.ports_client,
                     os.security_groups_client)
@@ -136,7 +143,8 @@
             self.creds_client.assign_user_role(user, project,
                                                self.admin_role)
             role_assigned = True
-            if self.identity_version == 'v3':
+            if (self.identity_version == 'v3' and
+                    CONF.identity.admin_domain_scope):
                 self.creds_client.assign_user_role_on_domain(
                     user, CONF.identity.admin_role)
         # Add roles specified in config file
diff --git a/tempest/common/preprov_creds.py b/tempest/common/preprov_creds.py
index 42c69d5..5992d24 100644
--- a/tempest/common/preprov_creds.py
+++ b/tempest/common/preprov_creds.py
@@ -86,10 +86,8 @@
         self.test_accounts_file = test_accounts_file
         if test_accounts_file:
             accounts = read_accounts_yaml(self.test_accounts_file)
-            self.use_default_creds = False
         else:
-            accounts = {}
-            self.use_default_creds = True
+            raise lib_exc.InvalidCredentials("No accounts file specified")
         self.hash_dict = self.get_hash_dict(
             accounts, admin_role, object_storage_operator_role,
             object_storage_reseller_admin_role)
@@ -165,12 +163,7 @@
         return hash_dict
 
     def is_multi_user(self):
-        # Default credentials is not a valid option with locking Account
-        if self.use_default_creds:
-            raise lib_exc.InvalidCredentials(
-                "Account file %s doesn't exist" % self.test_accounts_file)
-        else:
-            return len(self.hash_dict['creds']) > 1
+        return len(self.hash_dict['creds']) > 1
 
     def is_multi_tenant(self):
         return self.is_multi_user()
@@ -245,9 +238,6 @@
         return temp_creds
 
     def _get_creds(self, roles=None):
-        if self.use_default_creds:
-            raise lib_exc.InvalidCredentials(
-                "Account file %s doesn't exist" % self.test_accounts_file)
         useable_hashes = self._get_match_hash_list(roles)
         if len(useable_hashes) == 0:
             msg = 'No users configured for type/roles %s' % roles
@@ -329,12 +319,9 @@
         return self.get_creds_by_roles([self.admin_role])
 
     def is_role_available(self, role):
-        if self.use_default_creds:
-            return False
-        else:
-            if self.hash_dict['roles'].get(role):
-                return True
-            return False
+        if self.hash_dict['roles'].get(role):
+            return True
+        return False
 
     def admin_available(self):
         return self.is_role_available(self.admin_role)
diff --git a/tempest/config.py b/tempest/config.py
index 3810ceb..1f88871 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -166,6 +166,10 @@
     cfg.StrOpt('default_domain_id',
                default='default',
                help="ID of the default domain"),
+    cfg.BoolOpt('admin_domain_scope',
+                default=False,
+                help="Whether keystone identity v3 policy required "
+                     "a domain scoped token to use admin APIs")
 ]
 
 identity_feature_group = cfg.OptGroup(name='identity-feature-enabled',
diff --git a/tempest/lib/auth.py b/tempest/lib/auth.py
index 974ba82..425758f 100644
--- a/tempest/lib/auth.py
+++ b/tempest/lib/auth.py
@@ -1,4 +1,5 @@
 # Copyright 2014 Hewlett-Packard Development Company, L.P.
+# Copyright 2016 Rackspace Inc.
 # All Rights Reserved.
 #
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -179,7 +180,7 @@
         :param headers: HTTP headers of the request
         :param body: HTTP body in case of POST / PUT
         :param filters: select a base URL out of the catalog
-        :returns a Tuple (url, headers, body)
+        :return: a Tuple (url, headers, body)
         """
         orig_req = dict(url=url, headers=headers, body=body)
 
@@ -223,6 +224,7 @@
         Configure auth provider to provide alt authentication data
         on a part of the *next* auth_request. If credentials are None,
         set invalid data.
+
         :param request_part: request part to contain invalid auth: url,
                              headers, body
         :param auth_data: alternative auth_data from which to get the
@@ -260,7 +262,7 @@
                  disable_ssl_certificate_validation=None,
                  ca_certs=None, trace_requests=None, scope='project'):
         super(KeystoneAuthProvider, self).__init__(credentials, scope)
-        self.dsvm = disable_ssl_certificate_validation
+        self.dscv = disable_ssl_certificate_validation
         self.ca_certs = ca_certs
         self.trace_requests = trace_requests
         self.auth_url = auth_url
@@ -339,7 +341,7 @@
 
     def _auth_client(self, auth_url):
         return json_v2id.TokenClient(
-            auth_url, disable_ssl_certificate_validation=self.dsvm,
+            auth_url, disable_ssl_certificate_validation=self.dscv,
             ca_certs=self.ca_certs, trace_requests=self.trace_requests)
 
     def _auth_params(self):
@@ -368,18 +370,27 @@
     def base_url(self, filters, auth_data=None):
         """Base URL from catalog
 
+        :param filters: Used to filter results
+
         Filters can be:
-        - service: compute, image, etc
-        - region: the service region
-        - endpoint_type: adminURL, publicURL, internalURL
-        - api_version: replace catalog version with this
-        - skip_path: take just the base URL
+
+        - service: service type name such as compute, image, etc.
+        - region: service region name
+        - name: service name, only if service exists
+        - endpoint_type: type of endpoint such as
+            adminURL, publicURL, internalURL
+        - api_version: the version of api used to replace catalog version
+        - skip_path: skips the suffix path of the url and uses base URL
+
+        :rtype: string
+        :return: url with filters applied
         """
         if auth_data is None:
             auth_data = self.get_auth()
         token, _auth_data = auth_data
         service = filters.get('service')
         region = filters.get('region')
+        name = filters.get('name')
         endpoint_type = filters.get('endpoint_type', 'publicURL')
 
         if service is None:
@@ -388,17 +399,19 @@
         _base_url = None
         for ep in _auth_data['serviceCatalog']:
             if ep["type"] == service:
+                if name is not None and ep["name"] != name:
+                    continue
                 for _ep in ep['endpoints']:
                     if region is not None and _ep['region'] == region:
                         _base_url = _ep.get(endpoint_type)
                 if not _base_url:
-                    # No region matching, use the first
+                    # No region or name matching, use the first
                     _base_url = ep['endpoints'][0].get(endpoint_type)
                 break
         if _base_url is None:
             raise exceptions.EndpointNotFound(
-                "service: %s, region: %s, endpoint_type: %s" %
-                (service, region, endpoint_type))
+                "service: %s, region: %s, endpoint_type: %s, name: %s" %
+                (service, region, endpoint_type, name))
         return apply_url_filters(_base_url, filters)
 
     def is_expired(self, auth_data):
@@ -415,7 +428,7 @@
 
     def _auth_client(self, auth_url):
         return json_v3id.V3TokenClient(
-            auth_url, disable_ssl_certificate_validation=self.dsvm,
+            auth_url, disable_ssl_certificate_validation=self.dscv,
             ca_certs=self.ca_certs, trace_requests=self.trace_requests)
 
     def _auth_params(self):
@@ -489,18 +502,27 @@
         the auth_data. In such case, as long as the requested service is
         'identity', we can use the original auth URL to build the base_url.
 
+        :param filters: Used to filter results
+
         Filters can be:
-        - service: compute, image, etc
-        - region: the service region
-        - endpoint_type: adminURL, publicURL, internalURL
-        - api_version: replace catalog version with this
-        - skip_path: take just the base URL
+
+        - service: service type name such as compute, image, etc.
+        - region: service region name
+        - name: service name, only if service exists
+        - endpoint_type: type of endpoint such as
+            adminURL, publicURL, internalURL
+        - api_version: the version of api used to replace catalog version
+        - skip_path: skips the suffix path of the url and uses base URL
+
+        :rtype: string
+        :return: url with filters applied
         """
         if auth_data is None:
             auth_data = self.get_auth()
         token, _auth_data = auth_data
         service = filters.get('service')
         region = filters.get('region')
+        name = filters.get('name')
         endpoint_type = filters.get('endpoint_type', 'public')
 
         if service is None:
@@ -510,19 +532,38 @@
             endpoint_type = endpoint_type.replace('URL', '')
         _base_url = None
         catalog = _auth_data.get('catalog', [])
+
         # Select entries with matching service type
         service_catalog = [ep for ep in catalog if ep['type'] == service]
         if len(service_catalog) > 0:
-            service_catalog = service_catalog[0]['endpoints']
+            if name is not None:
+                service_catalog = (
+                    [ep for ep in service_catalog if ep['name'] == name])
+                if len(service_catalog) > 0:
+                    service_catalog = service_catalog[0]['endpoints']
+                else:
+                    raise exceptions.EndpointNotFound(name)
+            else:
+                service_catalog = service_catalog[0]['endpoints']
         else:
             if len(catalog) == 0 and service == 'identity':
                 # NOTE(andreaf) If there's no catalog at all and the service
                 # is identity, it's a valid use case. Having a non-empty
                 # catalog with no identity in it is not valid instead.
+                msg = ('Got an empty catalog. Scope: %s. '
+                       'Falling back to configured URL for %s: %s')
+                LOG.debug(msg, self.scope, service, self.auth_url)
                 return apply_url_filters(self.auth_url, filters)
             else:
                 # No matching service
-                raise exceptions.EndpointNotFound(service)
+                msg = ('No matching service found in the catalog.\n'
+                       'Scope: %s, Credentials: %s\n'
+                       'Auth data: %s\n'
+                       'Service: %s, Region: %s, endpoint_type: %s\n'
+                       'Catalog: %s')
+                raise exceptions.EndpointNotFound(msg % (
+                    self.scope, self.credentials, _auth_data, service, region,
+                    endpoint_type, catalog))
         # Filter by endpoint type (interface)
         filtered_catalog = [ep for ep in service_catalog if
                             ep['interface'] == endpoint_type]
@@ -533,7 +574,7 @@
         filtered_catalog = [ep for ep in filtered_catalog if
                             ep['region'] == region]
         if len(filtered_catalog) == 0:
-            # No matching region, take the first endpoint
+            # No matching region (or name), take the first endpoint
             filtered_catalog = [service_catalog[0]]
         # There should be only one match. If not take the first.
         _base_url = filtered_catalog[0].get('url', None)
@@ -590,9 +631,9 @@
     creds = credential_class(**kwargs)
     # Fill in the credentials fields that were not specified
     if fill_in:
-        dsvm = disable_ssl_certificate_validation
+        dscv = disable_ssl_certificate_validation
         auth_provider = auth_provider_class(
-            creds, auth_url, disable_ssl_certificate_validation=dsvm,
+            creds, auth_url, disable_ssl_certificate_validation=dscv,
             ca_certs=ca_certs, trace_requests=trace_requests)
         creds = auth_provider.fill_credentials()
     return creds
diff --git a/tempest/lib/common/rest_client.py b/tempest/lib/common/rest_client.py
index bf107d2..1b0f53a 100644
--- a/tempest/lib/common/rest_client.py
+++ b/tempest/lib/common/rest_client.py
@@ -54,6 +54,8 @@
     :param auth_provider: an auth provider object used to wrap requests in auth
     :param str service: The service name to use for the catalog lookup
     :param str region: The region to use for the catalog lookup
+    :param str name: The endpoint name to use for the catalog lookup; this
+                     returns only if the service exists
     :param str endpoint_type: The endpoint type to use for the catalog lookup
     :param int build_interval: Time in seconds between to status checks in
                                wait loops
@@ -76,10 +78,11 @@
                  endpoint_type='publicURL',
                  build_interval=1, build_timeout=60,
                  disable_ssl_certificate_validation=False, ca_certs=None,
-                 trace_requests=''):
+                 trace_requests='', name=None):
         self.auth_provider = auth_provider
         self.service = service
         self.region = region
+        self.name = name
         self.endpoint_type = endpoint_type
         self.build_interval = build_interval
         self.build_timeout = build_timeout
@@ -191,7 +194,8 @@
         _filters = dict(
             service=self.service,
             endpoint_type=self.endpoint_type,
-            region=self.region
+            region=self.region,
+            name=self.name
         )
         if self.api_version is not None:
             _filters['api_version'] = self.api_version
diff --git a/tempest/scenario/test_network_advanced_server_ops.py b/tempest/scenario/test_network_advanced_server_ops.py
index 4c2d31b..bfdb0c2 100644
--- a/tempest/scenario/test_network_advanced_server_ops.py
+++ b/tempest/scenario/test_network_advanced_server_ops.py
@@ -52,7 +52,10 @@
 
     def _setup_network_and_servers(self):
         keypair = self.create_keypair()
-        security_group = self._create_security_group()
+        security_groups = []
+        if test.is_extension_enabled('security-group', 'network'):
+            security_group = self._create_security_group()
+            security_groups = [{'name': security_group['name']}]
         network, subnet, router = self.create_networks()
         public_network_id = CONF.network.public_network_id
         server_name = data_utils.rand_name('server-smoke')
@@ -60,7 +63,7 @@
             name=server_name,
             networks=[{'uuid': network.id}],
             key_name=keypair['name'],
-            security_groups=[{'name': security_group['name']}],
+            security_groups=security_groups,
             wait_until='ACTIVE')
         floating_ip = self.create_floating_ip(server, public_network_id)
         # Verify that we can indeed connect to the server before we mess with
diff --git a/tempest/tests/cmd/test_run.py b/tempest/tests/cmd/test_run.py
new file mode 100644
index 0000000..9aa06e5
--- /dev/null
+++ b/tempest/tests/cmd/test_run.py
@@ -0,0 +1,110 @@
+# Copyright 2015 Hewlett-Packard Development Company, L.P.
+#
+# 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 argparse
+import os
+import shutil
+import subprocess
+import tempfile
+
+import mock
+
+from tempest.cmd import run
+from tempest.tests import base
+
+DEVNULL = open(os.devnull, 'wb')
+
+
+class TestTempestRun(base.TestCase):
+
+    def setUp(self):
+        super(TestTempestRun, self).setUp()
+        self.run_cmd = run.TempestRun(None, None)
+
+    def test_build_options(self):
+        args = mock.Mock(spec=argparse.Namespace)
+        setattr(args, "subunit", True)
+        setattr(args, "parallel", False)
+        setattr(args, "concurrency", 10)
+        options = self.run_cmd._build_options(args)
+        self.assertEqual(['--subunit',
+                          '--concurrency=10'],
+                         options)
+
+    def test__build_regex_default(self):
+        args = mock.Mock(spec=argparse.Namespace)
+        setattr(args, 'smoke', False)
+        setattr(args, 'regex', '')
+        self.assertEqual('', self.run_cmd._build_regex(args))
+
+    def test__build_regex_smoke(self):
+        args = mock.Mock(spec=argparse.Namespace)
+        setattr(args, "smoke", True)
+        setattr(args, 'regex', '')
+        self.assertEqual('smoke', self.run_cmd._build_regex(args))
+
+    def test__build_regex_regex(self):
+        args = mock.Mock(spec=argparse.Namespace)
+        setattr(args, 'smoke', False)
+        setattr(args, "regex", 'i_am_a_fun_little_regex')
+        self.assertEqual('i_am_a_fun_little_regex',
+                         self.run_cmd._build_regex(args))
+
+
+class TestRunReturnCode(base.TestCase):
+    def setUp(self):
+        super(TestRunReturnCode, self).setUp()
+        # Setup test dirs
+        self.directory = tempfile.mkdtemp(prefix='tempest-unit')
+        self.addCleanup(shutil.rmtree, self.directory)
+        self.test_dir = os.path.join(self.directory, 'tests')
+        os.mkdir(self.test_dir)
+        # Setup Test files
+        self.testr_conf_file = os.path.join(self.directory, '.testr.conf')
+        self.setup_cfg_file = os.path.join(self.directory, 'setup.cfg')
+        self.passing_file = os.path.join(self.test_dir, 'test_passing.py')
+        self.failing_file = os.path.join(self.test_dir, 'test_failing.py')
+        self.init_file = os.path.join(self.test_dir, '__init__.py')
+        self.setup_py = os.path.join(self.directory, 'setup.py')
+        shutil.copy('tempest/tests/files/testr-conf', self.testr_conf_file)
+        shutil.copy('tempest/tests/files/passing-tests', self.passing_file)
+        shutil.copy('tempest/tests/files/failing-tests', self.failing_file)
+        shutil.copy('setup.py', self.setup_py)
+        shutil.copy('tempest/tests/files/setup.cfg', self.setup_cfg_file)
+        shutil.copy('tempest/tests/files/__init__.py', self.init_file)
+        # Change directory, run wrapper and check result
+        self.addCleanup(os.chdir, os.path.abspath(os.curdir))
+        os.chdir(self.directory)
+
+    def assertRunExit(self, cmd, expected):
+        p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+                             stderr=subprocess.PIPE)
+        out, err = p.communicate()
+        msg = ("Running %s got an unexpected returncode\n"
+               "Stdout: %s\nStderr: %s" % (' '.join(cmd), out, err))
+        self.assertEqual(p.returncode, expected, msg)
+
+    def test_tempest_run_passes(self):
+        # Git init is required for the pbr testr command. pbr requires a git
+        # version or an sdist to work. so make the test directory a git repo
+        # too.
+        subprocess.call(['git', 'init'], stderr=DEVNULL)
+        self.assertRunExit(['tempest', 'run', '--regex', 'passing'], 0)
+
+    def test_tempest_run_fails(self):
+        # Git init is required for the pbr testr command. pbr requires a git
+        # version or an sdist to work. so make the test directory a git repo
+        # too.
+        subprocess.call(['git', 'init'], stderr=DEVNULL)
+        self.assertRunExit(['tempest', 'run'], 1)
diff --git a/tempest/tests/cmd/test_workspace.py b/tempest/tests/cmd/test_workspace.py
new file mode 100644
index 0000000..c4bd7b2
--- /dev/null
+++ b/tempest/tests/cmd/test_workspace.py
@@ -0,0 +1,124 @@
+# Copyright 2016 Rackspace
+#
+# 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 os
+import shutil
+import subprocess
+import tempfile
+
+from tempest.cmd.workspace import WorkspaceManager
+from tempest.lib.common.utils import data_utils
+from tempest.tests import base
+
+
+class TestTempestWorkspaceBase(base.TestCase):
+    def setUp(self):
+        super(TestTempestWorkspaceBase, self).setUp()
+        self.name = data_utils.rand_uuid()
+        self.path = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.path, ignore_errors=True)
+        store_dir = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, store_dir, ignore_errors=True)
+        self.store_file = os.path.join(store_dir, 'workspace.yaml')
+        self.workspace_manager = WorkspaceManager(path=self.store_file)
+        self.workspace_manager.register_new_workspace(self.name, self.path)
+
+
+class TestTempestWorkspace(TestTempestWorkspaceBase):
+    def _run_cmd_gets_return_code(self, cmd, expected):
+        process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+                                   stderr=subprocess.PIPE)
+        stdout, stderr = process.communicate()
+        return_code = process.returncode
+        msg = ("%s failled with:\nstdout: %s\nstderr: %s" % (' '.join(cmd),
+                                                             stdout, stderr))
+        self.assertEqual(return_code, expected, msg)
+
+    def test_run_workspace_list(self):
+        cmd = ['tempest', 'workspace', '--workspace-path',
+               self.store_file, 'list']
+        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]
+        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]
+        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))
+
+    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]
+        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]
+        self._run_cmd_gets_return_code(cmd, 0)
+        self.assertIsNone(self.workspace_manager.get_workspace(self.name))
+
+
+class TestTempestWorkspaceManager(TestTempestWorkspaceBase):
+    def setUp(self):
+        super(TestTempestWorkspaceManager, self).setUp()
+        self.name = data_utils.rand_uuid()
+        self.path = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.path, ignore_errors=True)
+        store_dir = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, store_dir, ignore_errors=True)
+        self.store_file = os.path.join(store_dir, 'workspace.yaml')
+        self.workspace_manager = WorkspaceManager(path=self.store_file)
+        self.workspace_manager.register_new_workspace(self.name, self.path)
+
+    def test_workspace_manager_get(self):
+        self.assertIsNotNone(self.workspace_manager.get_workspace(self.name))
+
+    def test_workspace_manager_rename(self):
+        new_name = data_utils.rand_uuid()
+        self.workspace_manager.rename_workspace(self.name, new_name)
+        self.assertIsNone(self.workspace_manager.get_workspace(self.name))
+        self.assertIsNotNone(self.workspace_manager.get_workspace(new_name))
+
+    def test_workspace_manager_move(self):
+        new_path = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, new_path, ignore_errors=True)
+        self.workspace_manager.move_workspace(self.name, new_path)
+        self.assertEqual(
+            self.workspace_manager.get_workspace(self.name), new_path)
+
+    def test_workspace_manager_remove(self):
+        self.workspace_manager.remove_workspace(self.name)
+        self.assertIsNone(self.workspace_manager.get_workspace(self.name))
+
+    def test_path_expansion(self):
+        name = data_utils.rand_uuid()
+        path = os.path.join("~", name)
+        os.makedirs(os.path.expanduser(path))
+        self.addCleanup(shutil.rmtree, path, ignore_errors=True)
+        self.workspace_manager.register_new_workspace(name, path)
+        self.assertIsNotNone(self.workspace_manager.get_workspace(name))
diff --git a/tempest/tests/lib/fake_identity.py b/tempest/tests/lib/fake_identity.py
index c903e47..831f8b5 100644
--- a/tempest/tests/lib/fake_identity.py
+++ b/tempest/tests/lib/fake_identity.py
@@ -100,7 +100,8 @@
 
     ],
     "type": "compute",
-    "id": "fake_compute_endpoint"
+    "id": "fake_compute_endpoint",
+    "name": "nova"
 }
 
 CATALOG_V3 = [COMPUTE_ENDPOINTS_V3, ]
diff --git a/tempest/tests/lib/test_auth.py b/tempest/tests/lib/test_auth.py
index c253187..c08bf6a 100644
--- a/tempest/tests/lib/test_auth.py
+++ b/tempest/tests/lib/test_auth.py
@@ -360,6 +360,58 @@
         self.assertRaises(exceptions.EndpointNotFound,
                           self._test_base_url_helper, None, self.filters)
 
+    def test_base_url_with_known_name(self):
+        """If name and service is known, return the endpoint."""
+        self.filters = {
+            'service': 'compute',
+            'endpoint_type': 'publicURL',
+            'region': 'FakeRegion',
+            'name': 'nova'
+        }
+        expected = self._get_result_url_from_endpoint(
+            self._endpoints[0]['endpoints'][1])
+        self._test_base_url_helper(expected, self.filters)
+
+    def test_base_url_with_known_name_and_unknown_servce(self):
+        """Test with Known Name and Unknown service
+
+        If the name is known but the service is unknown, raise an exception.
+        """
+        self.filters = {
+            'service': 'AintNoBodyKnowThatService',
+            'endpoint_type': 'publicURL',
+            'region': 'FakeRegion',
+            'name': 'AintNoBodyKnowThatName'
+        }
+        self.assertRaises(exceptions.EndpointNotFound,
+                          self._test_base_url_helper, None, self.filters)
+
+    def test_base_url_with_unknown_name_and_known_service(self):
+        """Test with Unknown Name and Known Service
+
+        If the name is unknown, raise an exception.  Note that filtering by
+        name is only successful service exists.
+        """
+
+        self.filters = {
+            'service': 'compute',
+            'endpoint_type': 'publicURL',
+            'region': 'FakeRegion',
+            'name': 'AintNoBodyKnowThatName'
+        }
+        self.assertRaises(exceptions.EndpointNotFound,
+                          self._test_base_url_helper, None, self.filters)
+
+    def test_base_url_without_name(self):
+        self.filters = {
+            'service': 'compute',
+            'endpoint_type': 'publicURL',
+            'region': 'FakeRegion',
+        }
+        expected = self._get_result_url_from_endpoint(
+            self._endpoints[0]['endpoints'][1])
+        self._test_base_url_helper(expected, self.filters)
+
     def test_base_url_with_api_version_filter(self):
         self.filters = {
             'service': 'compute',
diff --git a/tempest/tests/lib/test_rest_client.py b/tempest/tests/lib/test_rest_client.py
index 106a1e5..057f57b 100644
--- a/tempest/tests/lib/test_rest_client.py
+++ b/tempest/tests/lib/test_rest_client.py
@@ -633,6 +633,7 @@
         expected = {'api_version': 'v1',
                     'endpoint_type': 'publicURL',
                     'region': None,
+                    'name': None,
                     'service': None,
                     'skip_path': True}
         self.rest_client.skip_path()
@@ -643,6 +644,7 @@
         expected = {'api_version': 'v1',
                     'endpoint_type': 'publicURL',
                     'region': None,
+                    'name': None,
                     'service': None}
         self.assertEqual(expected, self.rest_client.filters)