Merge "Add documentation for glance api"
diff --git a/.gitignore b/.gitignore
index 5b87cec..e96deb1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,7 @@
 !.coveragerc
 cover/
 doc/source/_static/tempest.conf.sample
+doc/source/plugin-registry.rst
 
 # Files created by releasenotes build
 releasenotes/build
diff --git a/README.rst b/README.rst
index 725a890..53c7de5 100644
--- a/README.rst
+++ b/README.rst
@@ -1,13 +1,8 @@
 Tempest - The OpenStack Integration Test Suite
 ==============================================
 
-.. image:: https://img.shields.io/pypi/v/tempest.svg
-    :target: https://pypi.python.org/pypi/tempest/
-    :alt: Latest Version
-
-.. image:: https://img.shields.io/pypi/dm/tempest.svg
-    :target: https://pypi.python.org/pypi/tempest/
-    :alt: Downloads
+The documentation for Tempest is officially hosted at:
+http://docs.openstack.org/developer/tempest/
 
 This is a set of integration tests to be run against a live OpenStack
 cluster. Tempest has batteries of tests for OpenStack API validation,
@@ -63,19 +58,21 @@
    This can be done within a venv, but the assumption for this guide is that
    the Tempest cli entry point will be in your shell's PATH.
 
-#. Installing Tempest will create a /etc/tempest dir which will contain the
-   sample config file packaged with Tempest. The contents of /etc/tempest will
-   be copied to all local working dirs, so if there is any common configuration
-   you'd like to be shared between anyone setting up local Tempest working dirs
-   it's recommended that you copy or rename tempest.conf.sample to tempest.conf
-   and make those changes to that file in /etc/tempest
+#. Installing Tempest may create a /etc/tempest dir, however if one isn't
+   created you can create one or use ~/.tempest/etc or ~/.config/tempest in
+   place of /etc/tempest. If none of these dirs are created tempest will create
+   ~/.tempest/etc when it's needed. The contents of this dir will always
+   automatically be copied to all etc/ dirs in local workspaces as an initial
+   setup step. So if there is any common configuration you'd like to be shared
+   between local Tempest workspaces it's recommended that you pre-populate it
+   before running ``tempest init``.
 
-#. Setup a local working Tempest dir. This is done by using the tempest init
+#. Setup a local Tempest workspace. This is done by using the tempest init
    command::
 
     $ tempest init cloud-01
 
-   works the same as::
+   which also works the same as::
 
     $ mkdir cloud-01 && cd cloud-01 && tempest init
 
@@ -88,11 +85,23 @@
    config files located in the etc/ subdir created by the ``tempest init``
    command. Tempest is expecting a tempest.conf file in etc/ so if only a
    sample exists you must rename or copy it to tempest.conf before making
-   any changes to it otherwise Tempest will not know how to load it.
+   any changes to it otherwise Tempest will not know how to load it. For
+   details on configuring tempest refer to the :ref:`tempest-configuration`.
 
 #. Once the configuration is done you're now ready to run Tempest. This can
-   be done with testr directly or any `testr`_ based test runner, like
-   `ostestr`_. For example, from the working dir running::
+   be done using the :ref:`tempest_run` command. This can be done by either
+   running::
+
+     $ tempest run
+
+   from the Tempest workspace directory. Or you can use the ``--workspace``
+   argument to run in the workspace you created regarless of your current
+   working directory. For example::
+
+     $ tempest run --workspace cloud-01
+
+   There is also the option to use testr directly, or any `testr`_ based test
+   runner, like `ostestr`_. For example, from the workspace dir run::
 
      $ ostestr --regex '(?!.*\[.*\bslow\b.*\])(^tempest\.(api|scenario))'
 
diff --git a/doc/source/conf.py b/doc/source/conf.py
index eef3620..127613d 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -15,6 +15,17 @@
 import os
 import subprocess
 
+# Build the plugin registry
+def build_plugin_registry(app):
+    root_dir = os.path.dirname(
+        os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+    subprocess.call(['tools/generate-tempest-plugins-list.sh'], cwd=root_dir)
+
+def setup(app):
+    app.connect('builder-inited', build_plugin_registry)
+
+
+
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 60ff46b..f1ede06 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -2,19 +2,14 @@
 Tempest Testing Project
 =======================
 
-Contents:
+--------
+Overview
+--------
 
 .. toctree::
    :maxdepth: 2
 
    overview
-   HACKING
-   REVIEWING
-   plugin
-   plugin-registry
-   library
-   microversion_testing
-   test-removal
 
 ------------
 Field Guides
@@ -32,6 +27,10 @@
    field_guide/stress
    field_guide/unit_tests
 
+=========
+For users
+=========
+
 ---------------------------
 Tempest Configuration Guide
 ---------------------------
@@ -56,6 +55,41 @@
    workspace
    run
 
+==============
+For developers
+==============
+
+-----------
+Development
+-----------
+
+.. toctree::
+   :maxdepth: 2
+
+   HACKING
+   REVIEWING
+   microversion_testing
+   test-removal
+
+-------
+Plugins
+-------
+
+.. toctree::
+   :maxdepth: 2
+
+   plugin
+   plugin-registry
+
+-------
+Library
+-------
+
+.. toctree::
+   :maxdepth: 2
+
+   library
+
 ==================
 Indices and tables
 ==================
diff --git a/doc/source/plugin-registry.rst b/doc/source/plugin-registry.rst
deleted file mode 100644
index 517e5b8..0000000
--- a/doc/source/plugin-registry.rst
+++ /dev/null
@@ -1,23 +0,0 @@
-..
-  Note to patch submitters: this file is covered by a periodic proposal
-  job.  You should edit the files data/tempest-plugins-registry.footer
-  data/tempest-plugins-registry.header instead of this one.
-
-==========================
- Tempest Plugin Registry
-==========================
-
-Since we've created the external plugin mechanism, it's gotten used by
-a lot of projects. The following is a list of plugins that currently
-exist.
-
-Detected Plugins
-================
-
-The following will list plugins that a script has found in the openstack/
-namespace, which includes but is not limited to official OpenStack
-projects.
-
-+----------------------------+-------------------------------------------------------------------------+
-|Plugin Name                 |URL                                                                      |
-+----------------------------+-------------------------------------------------------------------------+
diff --git a/doc/source/run.rst b/doc/source/run.rst
index 07fa5f7..ce7f03e 100644
--- a/doc/source/run.rst
+++ b/doc/source/run.rst
@@ -1,3 +1,5 @@
+.. _tempest_run:
+
 -----------
 Tempest Run
 -----------
diff --git a/requirements.txt b/requirements.txt
index 7d01f69..84be219 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -14,7 +14,7 @@
 oslo.i18n>=2.1.0 # Apache-2.0
 oslo.log>=1.14.0 # Apache-2.0
 oslo.serialization>=1.10.0 # Apache-2.0
-oslo.utils>=3.11.0 # Apache-2.0
+oslo.utils>=3.14.0 # Apache-2.0
 six>=1.9.0 # MIT
 fixtures>=3.0.0 # Apache-2.0/BSD
 testscenarios>=0.4 # Apache-2.0/BSD
diff --git a/tempest/api/identity/admin/v3/test_projects.py b/tempest/api/identity/admin/v3/test_projects.py
index 607bebe..bee77df 100644
--- a/tempest/api/identity/admin/v3/test_projects.py
+++ b/tempest/api/identity/admin/v3/test_projects.py
@@ -13,10 +13,15 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import testtools
+
 from tempest.api.identity import base
 from tempest.common.utils import data_utils
+from tempest import config
 from tempest import test
 
+CONF = config.CONF
+
 
 class ProjectsTestJSON(base.BaseIdentityV3AdminTest):
 
@@ -52,6 +57,37 @@
         self.assertEqual(project_name, body['name'])
         self.assertEqual(self.data.domain['id'], body['domain_id'])
 
+    @testtools.skipUnless(CONF.identity_feature_enabled.reseller,
+                          'Reseller not available.')
+    @test.idempotent_id('1854f9c0-70bc-4d11-a08a-1c789d339e3d')
+    def test_project_create_with_parent(self):
+        # Create root project without providing a parent_id
+        self.data.setup_test_domain()
+        domain_id = self.data.domain['id']
+
+        root_project_name = data_utils.rand_name('root_project')
+        root_project = self.projects_client.create_project(
+            root_project_name, domain_id=domain_id)['project']
+        self.addCleanup(
+            self.projects_client.delete_project, root_project['id'])
+
+        root_project_id = root_project['id']
+        parent_id = root_project['parent_id']
+        self.assertEqual(root_project_name, root_project['name'])
+        # If not provided, the parent_id must point to the top level
+        # project in the hierarchy, i.e. its domain
+        self.assertEqual(domain_id, parent_id)
+
+        # Create a project using root_project_id as parent_id
+        project_name = data_utils.rand_name('project')
+        project = self.projects_client.create_project(
+            project_name, domain_id=domain_id,
+            parent_id=root_project_id)['project']
+        self.addCleanup(self.projects_client.delete_project, project['id'])
+        parent_id = project['parent_id']
+        self.assertEqual(project_name, project['name'])
+        self.assertEqual(root_project_id, parent_id)
+
     @test.idempotent_id('1f66dc76-50cc-4741-a200-af984509e480')
     def test_project_create_enabled(self):
         # Create a project that is enabled
diff --git a/tempest/api/identity/base.py b/tempest/api/identity/base.py
index 7a1e3a5..df39390 100644
--- a/tempest/api/identity/base.py
+++ b/tempest/api/identity/base.py
@@ -218,21 +218,21 @@
     def teardown_all(self):
         for user in self.users:
             test_utils.call_and_ignore_notfound_exc(
-                self.users_client.delete_user, user)
+                self.users_client.delete_user, user['id'])
         for tenant in self.tenants:
             test_utils.call_and_ignore_notfound_exc(
-                self.projects_client.delete_tenant, tenant)
+                self.projects_client.delete_tenant, tenant['id'])
         for project in reversed(self.projects):
             test_utils.call_and_ignore_notfound_exc(
-                self.projects_client.delete_project, project)
+                self.projects_client.delete_project, project['id'])
         for role in self.roles:
             test_utils.call_and_ignore_notfound_exc(
-                self.roles_client.delete_role, role)
+                self.roles_client.delete_role, role['id'])
         for domain in self.domains:
             test_utils.call_and_ignore_notfound_exc(
-                self.domains_client.update_domain, domain, enabled=False)
+                self.domains_client.update_domain, domain['id'], enabled=False)
             test_utils.call_and_ignore_notfound_exc(
-                self.domains_client.delete_domain, domain)
+                self.domains_client.delete_domain, domain['id'])
 
 
 class DataGeneratorV2(BaseDataGenerator):
diff --git a/tempest/api/network/test_allowed_address_pair.py b/tempest/api/network/test_allowed_address_pair.py
index b2892e5..92dfc56 100644
--- a/tempest/api/network/test_allowed_address_pair.py
+++ b/tempest/api/network/test_allowed_address_pair.py
@@ -14,6 +14,7 @@
 #    under the License.
 
 import netaddr
+import six
 
 from tempest.api.network import base
 from tempest import config
@@ -90,7 +91,8 @@
         body = self.ports_client.update_port(
             port_id, allowed_address_pairs=allowed_address_pairs)
         allowed_address_pair = body['port']['allowed_address_pairs']
-        self.assertEqual(allowed_address_pair, allowed_address_pairs)
+        six.assertCountEqual(self, allowed_address_pair,
+                             allowed_address_pairs)
 
     @test.idempotent_id('9599b337-272c-47fd-b3cf-509414414ac4')
     def test_update_port_with_address_pair(self):
diff --git a/tempest/api/volume/admin/test_volumes_backup.py b/tempest/api/volume/admin/test_volumes_backup.py
index 66bab51..b6dc488 100644
--- a/tempest/api/volume/admin/test_volumes_backup.py
+++ b/tempest/api/volume/admin/test_volumes_backup.py
@@ -27,17 +27,17 @@
 CONF = config.CONF
 
 
-class VolumesBackupsV2Test(base.BaseVolumeAdminTest):
+class VolumesBackupsAdminV2Test(base.BaseVolumeAdminTest):
 
     @classmethod
     def skip_checks(cls):
-        super(VolumesBackupsV2Test, cls).skip_checks()
+        super(VolumesBackupsAdminV2Test, cls).skip_checks()
         if not CONF.volume_feature_enabled.backup:
             raise cls.skipException("Cinder backup feature disabled")
 
     @classmethod
     def resource_setup(cls):
-        super(VolumesBackupsV2Test, cls).resource_setup()
+        super(VolumesBackupsAdminV2Test, cls).resource_setup()
 
         cls.volume = cls.create_volume()
 
@@ -167,5 +167,5 @@
                                                          'available')
 
 
-class VolumesBackupsV1Test(VolumesBackupsV2Test):
+class VolumesBackupsAdminV1Test(VolumesBackupsAdminV2Test):
     _api_version = 1
diff --git a/tempest/api/volume/base.py b/tempest/api/volume/base.py
index e9be529..087b9a8 100644
--- a/tempest/api/volume/base.py
+++ b/tempest/api/volume/base.py
@@ -76,6 +76,7 @@
         else:
             cls.snapshots_client = cls.os.snapshots_v2_client
             cls.volumes_client = cls.os.volumes_v2_client
+            cls.backups_client = cls.os.backups_v2_client
             cls.volumes_extension_client = cls.os.volumes_v2_extension_client
             cls.availability_zone_client = (
                 cls.os.volume_v2_availability_zone_client)
diff --git a/tempest/api/volume/test_volumes_backup.py b/tempest/api/volume/test_volumes_backup.py
new file mode 100644
index 0000000..87146db
--- /dev/null
+++ b/tempest/api/volume/test_volumes_backup.py
@@ -0,0 +1,72 @@
+# Copyright 2016 Red Hat, Inc.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.api.volume import base
+from tempest.common.utils import data_utils
+from tempest.common import waiters
+from tempest import config
+from tempest import test
+
+CONF = config.CONF
+
+
+class VolumesBackupsV2Test(base.BaseVolumeTest):
+
+    @classmethod
+    def skip_checks(cls):
+        super(VolumesBackupsV2Test, cls).skip_checks()
+        if not CONF.volume_feature_enabled.backup:
+            raise cls.skipException("Cinder backup feature disabled")
+
+    @classmethod
+    def resource_setup(cls):
+        super(VolumesBackupsV2Test, cls).resource_setup()
+
+        cls.volume = cls.create_volume()
+
+    @test.idempotent_id('07af8f6d-80af-44c9-a5dc-c8427b1b62e6')
+    @test.services('compute')
+    def test_backup_create_attached_volume(self):
+        """Test backup create using force flag.
+
+        Cinder allows to create a volume backup, whether the volume status
+        is "available" or "in-use".
+        """
+        # Create a server
+        server_name = data_utils.rand_name('instance')
+        server = self.create_server(name=server_name, wait_until='ACTIVE')
+        self.addCleanup(self.servers_client.delete_server, server['id'])
+        # Attach volume to instance
+        self.servers_client.attach_volume(server['id'],
+                                          volumeId=self.volume['id'])
+        waiters.wait_for_volume_status(self.volumes_client,
+                                       self.volume['id'], 'in-use')
+        self.addCleanup(waiters.wait_for_volume_status, self.volumes_client,
+                        self.volume['id'], 'available')
+        self.addCleanup(self.servers_client.detach_volume, server['id'],
+                        self.volume['id'])
+        # Create backup using force flag
+        backup_name = data_utils.rand_name('Backup')
+        backup = self.backups_client.create_backup(
+            volume_id=self.volume['id'],
+            name=backup_name, force=True)['backup']
+        self.addCleanup(self.backups_client.delete_backup, backup['id'])
+        self.backups_client.wait_for_backup_status(backup['id'],
+                                                   'available')
+        self.assertEqual(backup_name, backup['name'])
+
+
+class VolumesBackupsV1Test(VolumesBackupsV2Test):
+    _api_version = 1
diff --git a/tempest/api/volume/test_volumes_clone.py b/tempest/api/volume/test_volumes_clone.py
new file mode 100644
index 0000000..f38a068
--- /dev/null
+++ b/tempest/api/volume/test_volumes_clone.py
@@ -0,0 +1,44 @@
+# Copyright 2016 OpenStack Foundation
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.api.volume import base
+from tempest import config
+from tempest import test
+
+
+CONF = config.CONF
+
+
+class VolumesCloneTest(base.BaseVolumeTest):
+
+    @test.idempotent_id('9adae371-a257-43a5-9555-dc7c88e66e0e')
+    def test_create_from_volume(self):
+        # Creates a volume from another volume passing a size different from
+        # the source volume.
+        src_size = CONF.volume.volume_size
+
+        src_vol = self.create_volume(size=src_size)
+        # Destination volume bigger than source
+        dst_vol = self.create_volume(source_volid=src_vol['id'],
+                                     size=src_size + 1)
+
+        volume = self.volumes_client.show_volume(dst_vol['id'])['volume']
+        # Should allow
+        self.assertEqual(volume['source_volid'], src_vol['id'])
+        self.assertEqual(int(volume['size']), src_size + 1)
+
+
+class VolumesV1CloneTest(VolumesCloneTest):
+    _api_version = 1
diff --git a/tempest/api/volume/test_volumes_clone_negative.py b/tempest/api/volume/test_volumes_clone_negative.py
new file mode 100644
index 0000000..ee51e00
--- /dev/null
+++ b/tempest/api/volume/test_volumes_clone_negative.py
@@ -0,0 +1,42 @@
+# Copyright 2016 OpenStack Foundation
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.api.volume import base
+from tempest import config
+from tempest.lib import exceptions
+from tempest import test
+
+
+CONF = config.CONF
+
+
+class VolumesCloneTest(base.BaseVolumeTest):
+
+    @test.idempotent_id('9adae371-a257-43a5-459a-dc7c88e66e0e')
+    def test_create_from_volume_decreasing_size(self):
+        # Creates a volume from another volume passing a size different from
+        # the source volume.
+        src_size = CONF.volume.volume_size + 1
+        src_vol = self.create_volume(size=src_size)
+
+        # Destination volume smaller than source
+        self.assertRaises(exceptions.BadRequest,
+                          self.volumes_client.create_volume,
+                          size=src_size - 1,
+                          source_volid=src_vol['id'])
+
+
+class VolumesV1CloneTest(VolumesCloneTest):
+    _api_version = 1
diff --git a/tempest/api/volume/test_volumes_snapshots.py b/tempest/api/volume/test_volumes_snapshots.py
index 0f7c4f6..c7f1e6e 100644
--- a/tempest/api/volume/test_volumes_snapshots.py
+++ b/tempest/api/volume/test_volumes_snapshots.py
@@ -178,16 +178,19 @@
 
     @test.idempotent_id('677863d1-3142-456d-b6ac-9924f667a7f4')
     def test_volume_from_snapshot(self):
-        # Create a temporary snap using wrapper method from base, then
-        # create a snap based volume and deletes it
-        snapshot = self.create_snapshot(self.volume_origin['id'])
-        # NOTE(gfidente): size is required also when passing snapshot_id
-        volume = self.volumes_client.create_volume(
-            snapshot_id=snapshot['id'])['volume']
-        waiters.wait_for_volume_status(self.volumes_client,
-                                       volume['id'], 'available')
-        self.delete_volume(self.volumes_client, volume['id'])
-        self.cleanup_snapshot(snapshot)
+        # Creates a volume a snapshot passing a size different from the source
+        src_size = CONF.volume.volume_size
+
+        src_vol = self.create_volume(size=src_size)
+        src_snap = self.create_snapshot(src_vol['id'])
+        # Destination volume bigger than source snapshot
+        dst_vol = self.create_volume(snapshot_id=src_snap['id'],
+                                     size=src_size + 1)
+
+        volume = self.volumes_client.show_volume(dst_vol['id'])['volume']
+        # Should allow
+        self.assertEqual(volume['snapshot_id'], src_snap['id'])
+        self.assertEqual(int(volume['size']), src_size + 1)
 
     @test.idempotent_id('db4d8e0a-7a2e-41cc-a712-961f6844e896')
     def test_snapshot_list_param_limit(self):
diff --git a/tempest/api/volume/test_volumes_snapshots_negative.py b/tempest/api/volume/test_volumes_snapshots_negative.py
index 374979c..2df9523 100644
--- a/tempest/api/volume/test_volumes_snapshots_negative.py
+++ b/tempest/api/volume/test_volumes_snapshots_negative.py
@@ -46,6 +46,20 @@
                           self.snapshots_client.create_snapshot,
                           volume_id=None, display_name=s_name)
 
+    @test.idempotent_id('677863d1-34f9-456d-b6ac-9924f667a7f4')
+    def test_volume_from_snapshot_decreasing_size(self):
+        # Creates a volume a snapshot passing a size different from the source
+        src_size = CONF.volume.volume_size + 1
+
+        src_vol = self.create_volume(size=src_size)
+        src_snap = self.create_snapshot(src_vol['id'])
+
+        # Destination volume smaller than source
+        self.assertRaises(lib_exc.BadRequest,
+                          self.volumes_client.create_volume,
+                          size=src_size - 1,
+                          snapshot_id=src_snap['id'])
+
 
 class VolumesV1SnapshotNegativeTestJSON(VolumesV2SnapshotNegativeTestJSON):
     _api_version = 1
diff --git a/tempest/cmd/run.py b/tempest/cmd/run.py
index 5580cf7..2eb122e 100644
--- a/tempest/cmd/run.py
+++ b/tempest/cmd/run.py
@@ -54,6 +54,24 @@
 If you want to adjust the number of workers use the **--concurrency** option
 and if you want to run tests serially use **--serial**
 
+Running with Workspaces
+-----------------------
+Tempest run enables you to run your tempest tests from any setup tempest
+workspace it relies on you having setup a tempest workspace with either the
+``tempest init`` or ``tempest workspace`` commands. Then using the
+``--workspace`` CLI option you can specify which one of your workspaces you
+want to run tempest from. Using this option you don't have to run Tempest
+directly with you current working directory being the workspace, Tempest will
+take care of managing everything to be executed from there.
+
+Running from Anywhere
+---------------------
+Tempest run provides you with an option to execute tempest from anywhere on
+your system. You are required to provide a config file in this case with the
+``--config-file`` option. When run tempest will create a .testrepository
+directory and a .testr.conf file in your current working directory. This way
+you can use testr commands directly to inspect the state of the previous run.
+
 Test Output
 ===========
 By default tempest run's output to STDOUT will be generated using the
@@ -73,6 +91,8 @@
 from oslo_log import log as logging
 from testrepository.commands import run_argv
 
+from tempest.cmd import init
+from tempest.cmd import workspace
 from tempest import config
 
 
@@ -82,7 +102,9 @@
 
 class TempestRun(command.Command):
 
-    def _set_env(self):
+    def _set_env(self, config_file=None):
+        if config_file:
+            CONF.set_config_path(os.path.abspath(config_file))
         # NOTE(mtreinish): This is needed so that testr doesn't gobble up any
         # stacktraces on failure.
         if 'TESTR_PDB' in os.environ:
@@ -90,18 +112,45 @@
         else:
             os.environ["TESTR_PDB"] = ""
 
+    def _create_testrepository(self):
+        if not os.path.isdir('.testrepository'):
+            returncode = run_argv(['testr', 'init'], sys.stdin, sys.stdout,
+                                  sys.stderr)
+            if returncode:
+                sys.exit(returncode)
+
+    def _create_testr_conf(self):
+        top_level_path = os.path.dirname(os.path.dirname(__file__))
+        discover_path = os.path.join(top_level_path, 'test_discover')
+        file_contents = init.TESTR_CONF % (top_level_path, discover_path)
+        with open('.testr.conf', 'w+') as testr_conf_file:
+                testr_conf_file.write(file_contents)
+
     def take_action(self, parsed_args):
-        self._set_env()
         returncode = 0
+        if parsed_args.config_file:
+            self._set_env(parsed_args.config_file)
+        else:
+            self._set_env()
+        # Workspace execution mode
+        if parsed_args.workspace:
+            workspace_mgr = workspace.WorkspaceManager(
+                parsed_args.workspace_path)
+            path = workspace_mgr.get_workspace(parsed_args.workspace)
+            os.chdir(path)
+            # NOTE(mtreinish): tempest init should create a .testrepository dir
+            # but since workspaces can be imported let's sanity check and
+            # ensure that one is created
+            self._create_testrepository()
         # Local execution mode
-        if os.path.isfile('.testr.conf'):
+        elif 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)
+            self._create_testrepository()
+        # local execution with config file mode
+        elif parsed_args.config_file:
+            self._create_testr_conf()
+            self._create_testrepository()
         else:
             print("No .testr.conf file was found for local execution")
             sys.exit(2)
@@ -124,6 +173,18 @@
         return parser
 
     def _add_args(self, parser):
+        # workspace args
+        parser.add_argument('--workspace', default=None,
+                            help='Name of tempest workspace to use for running'
+                                 ' tests. You can see a list of workspaces '
+                                 'with tempest workspace list')
+        parser.add_argument('--workspace-path', default=None,
+                            dest='workspace_path',
+                            help="The path to the workspace file, the default "
+                                 "is ~/.tempest/workspace.yaml")
+        # Configuration flags
+        parser.add_argument('--config-file', default=None, dest='config_file',
+                            help='Configuration file to run tempest with')
         # test selection args
         regex = parser.add_mutually_exclusive_group()
         regex.add_argument('--smoke', action='store_true',
@@ -144,7 +205,7 @@
         parser.add_argument('--list-tests', '-l', action='store_true',
                             help='List tests',
                             default=False)
-        # exectution args
+        # execution args
         parser.add_argument('--concurrency', '-w',
                             help="The number of workers to use, defaults to "
                                  "the number of cpus")
diff --git a/tempest/cmd/subunit_describe_calls.py b/tempest/cmd/subunit_describe_calls.py
index c990add..da7f426 100644
--- a/tempest/cmd/subunit_describe_calls.py
+++ b/tempest/cmd/subunit_describe_calls.py
@@ -45,6 +45,7 @@
 
 Ports file JSON structure
 ^^^^^^^^^^^^^^^^^^^^^^^^^
+::
 
   {
       "<port number>": "<name of service>",
@@ -54,6 +55,8 @@
 
 Output file JSON structure
 ^^^^^^^^^^^^^^^^^^^^^^^^^^
+::
+
   {
       "full_test_name[with_id_and_tags]": [
           {
diff --git a/tempest/common/fixed_network.py b/tempest/common/fixed_network.py
index 5f0685e..f57c18a 100644
--- a/tempest/common/fixed_network.py
+++ b/tempest/common/fixed_network.py
@@ -14,7 +14,7 @@
 from oslo_log import log as logging
 
 from tempest import exceptions
-from tempest.lib.common.utils import misc as misc_utils
+from tempest.lib.common.utils import test_utils
 
 LOG = logging.getLogger(__name__)
 
@@ -31,7 +31,7 @@
         list returns a 404, there are no found networks, or the found network
         is invalid
     """
-    caller = misc_utils.find_test_caller()
+    caller = test_utils.find_test_caller()
 
     if not name:
         raise exceptions.InvalidTestResource(type='network', name=name)
@@ -84,7 +84,7 @@
            tenant network is available in the creds provider
     :returns: a dict with 'id' and 'name' of the network
     """
-    caller = misc_utils.find_test_caller()
+    caller = test_utils.find_test_caller()
     net_creds = creds_provider.get_primary_creds()
     network = getattr(net_creds, 'network', None)
     if not network or not network.get('name'):
diff --git a/tempest/common/image.py b/tempest/common/image.py
index 72e3a72..95a7d1a 100644
--- a/tempest/common/image.py
+++ b/tempest/common/image.py
@@ -47,6 +47,11 @@
     fields_copy = copy.deepcopy(metadata)
 
     copy_from = fields_copy.pop('copy_from', None)
+    purge = fields_copy.pop('purge_props', None)
+
+    if purge is not None:
+        headers['x-glance-registry-purge-props'] = purge
+
     if copy_from is not None:
         headers['x-glance-api-copy-from'] = copy_from
 
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index e083167..df08e30 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -18,7 +18,7 @@
 from tempest.common import image as common_image
 from tempest import config
 from tempest import exceptions
-from tempest.lib.common.utils import misc as misc_utils
+from tempest.lib.common.utils import test_utils
 from tempest.lib import exceptions as lib_exc
 from tempest.lib.services.image.v1 import images_client as images_v1_client
 
@@ -91,7 +91,7 @@
                         'timeout': timeout})
             message += ' Current status: %s.' % server_status
             message += ' Current task state: %s.' % task_state
-            caller = misc_utils.find_test_caller()
+            caller = test_utils.find_test_caller()
             if caller:
                 message = '(%s) %s' % (caller, message)
             raise exceptions.TimeoutException(message)
@@ -162,7 +162,7 @@
                                           'status': status,
                                           'current_status': current_status,
                                           'timeout': client.build_timeout})
-    caller = misc_utils.find_test_caller()
+    caller = test_utils.find_test_caller()
     if caller:
         message = '(%s) %s' % (caller, message)
     raise exceptions.TimeoutException(message)
@@ -235,7 +235,7 @@
                         'status': status,
                         'timeout': client.build_timeout})
             message += ' Current state of %s: %s.' % (attr, status_curr)
-            caller = misc_utils.find_test_caller()
+            caller = test_utils.find_test_caller()
             if caller:
                 message = '(%s) %s' % (caller, message)
             raise exceptions.TimeoutException(message)
diff --git a/tempest/config.py b/tempest/config.py
index eb5e23a..8ec8b24 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -191,7 +191,12 @@
                 help="A list of enabled identity extensions with a special "
                      "entry all which indicates every extension is enabled. "
                      "Empty list indicates all extensions are disabled. "
-                     "To get the list of extensions run: 'keystone discover'")
+                     "To get the list of extensions run: 'keystone discover'"),
+    # TODO(rodrigods): Remove the reseller flag when Kilo and Liberty is end
+    # of life.
+    cfg.BoolOpt('reseller',
+                default=False,
+                help='Does the environment support reseller?')
 ]
 
 compute_group = cfg.OptGroup(name='compute',
diff --git a/tempest/services/identity/v2/json/roles_client.py b/tempest/services/identity/v2/json/roles_client.py
deleted file mode 100644
index 15c8834..0000000
--- a/tempest/services/identity/v2/json/roles_client.py
+++ /dev/null
@@ -1,107 +0,0 @@
-#    Licensed under the Apache License, Version 2.0 (the "License"); you may
-#    not use this file except in compliance with the License. You may obtain
-#    a copy of the License at
-#
-#         http://www.apache.org/licenses/LICENSE-2.0
-#
-#    Unless required by applicable law or agreed to in writing, software
-#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-#    License for the specific language governing permissions and limitations
-#    under the License.
-
-from oslo_serialization import jsonutils as json
-from six.moves.urllib import parse as urllib
-
-from tempest.lib.common import rest_client
-
-
-class RolesClient(rest_client.RestClient):
-    api_version = "v2.0"
-
-    def create_role(self, **kwargs):
-        """Create a role.
-
-        Available params: see http://developer.openstack.org/
-                              api-ref-identity-v2-ext.html#createRole
-        """
-        post_body = json.dumps({'role': kwargs})
-        resp, body = self.post('OS-KSADM/roles', post_body)
-        self.expected_success(200, resp.status)
-        body = json.loads(body)
-        return rest_client.ResponseBody(resp, body)
-
-    def show_role(self, role_id_or_name):
-        """Get a role by its id or name.
-
-        Available params: see
-            http://developer.openstack.org/
-            api-ref-identity-v2-ext.html#showRoleByID
-            OR
-            http://developer.openstack.org/
-            api-ref-identity-v2-ext.html#showRoleByName
-        """
-        resp, body = self.get('OS-KSADM/roles/%s' % role_id_or_name)
-        self.expected_success(200, resp.status)
-        body = json.loads(body)
-        return rest_client.ResponseBody(resp, body)
-
-    def list_roles(self, **params):
-        """Returns roles.
-
-        Available params: see http://developer.openstack.org/
-                              api-ref-identity-v2-ext.html#listRoles
-        """
-        url = 'OS-KSADM/roles'
-        if params:
-            url += '?%s' % urllib.urlencode(params)
-        resp, body = self.get(url)
-        self.expected_success(200, resp.status)
-        body = json.loads(body)
-        return rest_client.ResponseBody(resp, body)
-
-    def delete_role(self, role_id):
-        """Delete a role.
-
-        Available params: see http://developer.openstack.org/
-                              api-ref-identity-v2-ext.html#deleteRole
-        """
-        resp, body = self.delete('OS-KSADM/roles/%s' % str(role_id))
-        self.expected_success(204, resp.status)
-        return rest_client.ResponseBody(resp, body)
-
-    def create_user_role_on_project(self, tenant_id, user_id, role_id):
-        """Add roles to a user on a tenant.
-
-        Available params: see
-            http://developer.openstack.org/
-            api-ref-identity-v2-ext.html#grantRoleToUserOnTenant
-        """
-        resp, body = self.put('/tenants/%s/users/%s/roles/OS-KSADM/%s' %
-                              (tenant_id, user_id, role_id), "")
-        self.expected_success(200, resp.status)
-        body = json.loads(body)
-        return rest_client.ResponseBody(resp, body)
-
-    def list_user_roles_on_project(self, tenant_id, user_id, **params):
-        """Returns a list of roles assigned to a user for a tenant."""
-        # TODO(gmann): Need to write API-ref link, Bug# 1592711
-        url = '/tenants/%s/users/%s/roles' % (tenant_id, user_id)
-        if params:
-            url += '?%s' % urllib.urlencode(params)
-        resp, body = self.get(url)
-        self.expected_success(200, resp.status)
-        body = json.loads(body)
-        return rest_client.ResponseBody(resp, body)
-
-    def delete_role_from_user_on_project(self, tenant_id, user_id, role_id):
-        """Removes a role assignment for a user on a tenant.
-
-        Available params: see
-            http://developer.openstack.org/
-            api-ref-identity-v2-ext.html#revokeRoleFromUserOnTenant
-        """
-        resp, body = self.delete('/tenants/%s/users/%s/roles/OS-KSADM/%s' %
-                                 (tenant_id, user_id, role_id))
-        self.expected_success(204, resp.status)
-        return rest_client.ResponseBody(resp, body)
diff --git a/tempest/services/identity/v2/json/tenants_client.py b/tempest/services/identity/v2/json/tenants_client.py
deleted file mode 100644
index 77ddaa5..0000000
--- a/tempest/services/identity/v2/json/tenants_client.py
+++ /dev/null
@@ -1,98 +0,0 @@
-# Copyright 2015 Red Hat, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from oslo_serialization import jsonutils as json
-from six.moves.urllib import parse as urllib
-
-from tempest.lib.common import rest_client
-
-
-class TenantsClient(rest_client.RestClient):
-    api_version = "v2.0"
-
-    def create_tenant(self, **kwargs):
-        """Create a tenant
-
-        Available params: see http://developer.openstack.org/
-                              api-ref-identity-v2-ext.html#createTenant
-        """
-        post_body = json.dumps({'tenant': kwargs})
-        resp, body = self.post('tenants', post_body)
-        self.expected_success(200, resp.status)
-        body = json.loads(body)
-        return rest_client.ResponseBody(resp, body)
-
-    def delete_tenant(self, tenant_id):
-        """Delete a tenant.
-
-        Available params: see http://developer.openstack.org/
-                              api-ref-identity-v2-ext.html#deleteTenant
-        """
-        resp, body = self.delete('tenants/%s' % str(tenant_id))
-        self.expected_success(204, resp.status)
-        return rest_client.ResponseBody(resp, body)
-
-    def show_tenant(self, tenant_id):
-        """Get tenant details.
-
-        Available params: see
-            http://developer.openstack.org/
-            api-ref-identity-v2-ext.html#admin-showTenantById
-        """
-        resp, body = self.get('tenants/%s' % str(tenant_id))
-        self.expected_success(200, resp.status)
-        body = json.loads(body)
-        return rest_client.ResponseBody(resp, body)
-
-    def list_tenants(self, **params):
-        """Returns tenants.
-
-        Available params: see http://developer.openstack.org/
-                              api-ref-identity-v2-ext.html#admin-listTenants
-        """
-        url = 'tenants'
-        if params:
-            url += '?%s' % urllib.urlencode(params)
-        resp, body = self.get(url)
-        self.expected_success(200, resp.status)
-        body = json.loads(body)
-        return rest_client.ResponseBody(resp, body)
-
-    def update_tenant(self, tenant_id, **kwargs):
-        """Updates a tenant.
-
-        Available params: see http://developer.openstack.org/
-                              api-ref-identity-v2-ext.html#updateTenant
-        """
-        if 'id' not in kwargs:
-            kwargs['id'] = tenant_id
-        post_body = json.dumps({'tenant': kwargs})
-        resp, body = self.post('tenants/%s' % tenant_id, post_body)
-        self.expected_success(200, resp.status)
-        body = json.loads(body)
-        return rest_client.ResponseBody(resp, body)
-
-    def list_tenant_users(self, tenant_id, **params):
-        """List users for a Tenant.
-
-        Available params: see http://developer.openstack.org/
-                              api-ref-identity-v2-ext.html#listUsersForTenant
-        """
-        url = '/tenants/%s/users' % tenant_id
-        if params:
-            url += '?%s' % urllib.urlencode(params)
-        resp, body = self.get(url)
-        self.expected_success(200, resp.status)
-        body = json.loads(body)
-        return rest_client.ResponseBody(resp, body)
diff --git a/tempest/services/identity/v2/json/users_client.py b/tempest/services/identity/v2/json/users_client.py
deleted file mode 100644
index 4ea17f9..0000000
--- a/tempest/services/identity/v2/json/users_client.py
+++ /dev/null
@@ -1,152 +0,0 @@
-#    Licensed under the Apache License, Version 2.0 (the "License"); you may
-#    not use this file except in compliance with the License. You may obtain
-#    a copy of the License at
-#
-#         http://www.apache.org/licenses/LICENSE-2.0
-#
-#    Unless required by applicable law or agreed to in writing, software
-#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-#    License for the specific language governing permissions and limitations
-#    under the License.
-
-from oslo_serialization import jsonutils as json
-from six.moves.urllib import parse as urllib
-
-from tempest.lib.common import rest_client
-
-
-class UsersClient(rest_client.RestClient):
-    api_version = "v2.0"
-
-    def create_user(self, **kwargs):
-        """Create a user.
-
-        Available params: see http://developer.openstack.org/
-                              api-ref-identity-admin-v2.html#admin-createUser
-        """
-        post_body = json.dumps({'user': kwargs})
-        resp, body = self.post('users', post_body)
-        self.expected_success(200, resp.status)
-        body = json.loads(body)
-        return rest_client.ResponseBody(resp, body)
-
-    def update_user(self, user_id, **kwargs):
-        """Updates a user.
-
-        Available params: see http://developer.openstack.org/
-                              api-ref-identity-admin-v2.html#admin-updateUser
-        """
-        put_body = json.dumps({'user': kwargs})
-        resp, body = self.put('users/%s' % user_id, put_body)
-        self.expected_success(200, resp.status)
-        body = json.loads(body)
-        return rest_client.ResponseBody(resp, body)
-
-    def show_user(self, user_id):
-        """GET a user.
-
-        Available params: see http://developer.openstack.org/
-                              api-ref-identity-admin-v2.html#admin-showUser
-        """
-        resp, body = self.get("users/%s" % user_id)
-        self.expected_success(200, resp.status)
-        body = json.loads(body)
-        return rest_client.ResponseBody(resp, body)
-
-    def delete_user(self, user_id):
-        """Delete a user.
-
-        Available params: see http://developer.openstack.org/
-                              api-ref-identity-admin-v2.html#admin-deleteUser
-        """
-        resp, body = self.delete("users/%s" % user_id)
-        self.expected_success(204, resp.status)
-        return rest_client.ResponseBody(resp, body)
-
-    def list_users(self, **params):
-        """Get the list of users.
-
-        Available params: see http://developer.openstack.org/
-                              api-ref-identity-admin-v2.html#admin-listUsers
-        """
-        url = "users"
-        if params:
-            url += '?%s' % urllib.urlencode(params)
-        resp, body = self.get(url)
-        self.expected_success(200, resp.status)
-        body = json.loads(body)
-        return rest_client.ResponseBody(resp, body)
-
-    def update_user_enabled(self, user_id, **kwargs):
-        """Enables or disables a user.
-
-        Available params: see http://developer.openstack.org/
-                              api-ref-identity-v2-ext.html#enableUser
-        """
-        # NOTE: The URL (users/<id>/enabled) is different from the api-site
-        # one (users/<id>/OS-KSADM/enabled) , but they are the same API
-        # because of the fact that in keystone/contrib/admin_crud/core.py
-        # both api use same action='set_user_enabled'
-        put_body = json.dumps({'user': kwargs})
-        resp, body = self.put('users/%s/enabled' % user_id, put_body)
-        self.expected_success(200, resp.status)
-        body = json.loads(body)
-        return rest_client.ResponseBody(resp, body)
-
-    def update_user_password(self, user_id, **kwargs):
-        """Update User Password."""
-        # TODO(piyush): Current api-site doesn't contain this API description.
-        # After fixing the api-site, we need to fix here also for putting the
-        # link to api-site.
-        # LP: https://bugs.launchpad.net/openstack-api-site/+bug/1524147
-        put_body = json.dumps({'user': kwargs})
-        resp, body = self.put('users/%s/OS-KSADM/password' % user_id, put_body)
-        self.expected_success(200, resp.status)
-        body = json.loads(body)
-        return rest_client.ResponseBody(resp, body)
-
-    def update_user_own_password(self, user_id, **kwargs):
-        """User updates own password"""
-        # TODO(piyush): Current api-site doesn't contain this API description.
-        # After fixing the api-site, we need to fix here also for putting the
-        # link to api-site.
-        # LP: https://bugs.launchpad.net/openstack-api-site/+bug/1524153
-        # NOTE: This API is used for updating user password by itself.
-        # Ref: http://lists.openstack.org/pipermail/openstack-dev/2015-December
-        #      /081803.html
-        patch_body = json.dumps({'user': kwargs})
-        resp, body = self.patch('OS-KSCRUD/users/%s' % user_id, patch_body)
-        self.expected_success(200, resp.status)
-        body = json.loads(body)
-        return rest_client.ResponseBody(resp, body)
-
-    def create_user_ec2_credential(self, user_id, **kwargs):
-        # TODO(piyush): Current api-site doesn't contain this API description.
-        # After fixing the api-site, we need to fix here also for putting the
-        # link to api-site.
-        post_body = json.dumps(kwargs)
-        resp, body = self.post('/users/%s/credentials/OS-EC2' % user_id,
-                               post_body)
-        self.expected_success(200, resp.status)
-        body = json.loads(body)
-        return rest_client.ResponseBody(resp, body)
-
-    def delete_user_ec2_credential(self, user_id, access):
-        resp, body = self.delete('/users/%s/credentials/OS-EC2/%s' %
-                                 (user_id, access))
-        self.expected_success(204, resp.status)
-        return rest_client.ResponseBody(resp, body)
-
-    def list_user_ec2_credentials(self, user_id):
-        resp, body = self.get('/users/%s/credentials/OS-EC2' % user_id)
-        self.expected_success(200, resp.status)
-        body = json.loads(body)
-        return rest_client.ResponseBody(resp, body)
-
-    def show_user_ec2_credential(self, user_id, access):
-        resp, body = self.get('/users/%s/credentials/OS-EC2/%s' %
-                              (user_id, access))
-        self.expected_success(200, resp.status)
-        body = json.loads(body)
-        return rest_client.ResponseBody(resp, body)
diff --git a/tempest/tests/common/test_image.py b/tempest/tests/common/test_image.py
index 34772a2..240df4d 100644
--- a/tempest/tests/common/test_image.py
+++ b/tempest/tests/common/test_image.py
@@ -46,7 +46,8 @@
             disk_format='vhd',
             copy_from='http://localhost/images/10',
             properties={'foo': 'bar'},
-            api={'abc': 'def'})
+            api={'abc': 'def'},
+            purge_props=True)
 
         expected = {
             'x-image-meta-name': 'test',
@@ -54,6 +55,7 @@
             'x-image-meta-disk_format': 'vhd',
             'x-glance-api-copy-from': 'http://localhost/images/10',
             'x-image-meta-property-foo': 'bar',
-            'x-glance-api-property-abc': 'def'
+            'x-glance-api-property-abc': 'def',
+            'x-glance-registry-purge-props': True
         }
         self.assertEqual(expected, observed)