Merge "Moving scenario docstring under the relevant test method"
diff --git a/HACKING.rst b/HACKING.rst
index 432db7d..7ab420b 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -166,8 +166,33 @@
 suite, Tempest is not suitable for handling all negative test cases, as
 the wide variety and complexity of negative tests can lead to long test
 runs and knowledge of internal implementation details. The bulk of
-negative testing should be handled with project function tests. The
-exception to this rule is API tests used for interoperability testing.
+negative testing should be handled with project function tests.
+All negative tests should be based on `API-WG guideline`_ . Such negative
+tests can block any changes from accurate failure code to invalid one.
+
+.. _API-WG guideline: https://github.com/openstack/api-wg/blob/master/guidelines/http.rst#failure-code-clarifications
+
+If facing some gray area which is not clarified on the above guideline, propose
+a new guideline to the API-WG. With a proposal to the API-WG we will be able to
+build a consensus across all OpenStack projects and improve the quality and
+consistency of all the APIs.
+
+In addition, we have some guidelines for additional negative tests.
+
+- About BadRequest(HTTP400) case: We can add a single negative tests of
+  BadRequest for each resource and method(POST, PUT).
+  Please don't implement more negative tests on the same combination of
+  resource and method even if API request parameters are different from
+  the existing test.
+- About NotFound(HTTP404) case: We can add a single negative tests of
+  NotFound for each resource and method(GET, PUT, DELETE, HEAD).
+  Please don't implement more negative tests on the same combination
+  of resource and method.
+
+The above guidelines don't cover all cases and we will grow these guidelines
+organically over time. Patches outside of the above guidelines are left up to
+the reviewers' discretion and if we face some conflicts between reviewers, we
+will expand the guideline based on our discussion and experience.
 
 Test skips because of Known Bugs
 --------------------------------
diff --git a/releasenotes/notes/12.1.0-remove-trove-tests-666522e9113549f9.yaml b/releasenotes/notes/12.1.0-remove-trove-tests-666522e9113549f9.yaml
index 1157a4f..7a1fc36 100644
--- a/releasenotes/notes/12.1.0-remove-trove-tests-666522e9113549f9.yaml
+++ b/releasenotes/notes/12.1.0-remove-trove-tests-666522e9113549f9.yaml
@@ -1,4 +1,4 @@
 ---
 upgrade:
   - All tests for the Trove project have been removed from tempest. They now
-    live as a tempest plugin in the the trove project.
+    live as a tempest plugin in the trove project.
diff --git a/releasenotes/notes/add-volume-clients-as-a-library-d05b6bc35e66c6ef.yaml b/releasenotes/notes/add-volume-clients-as-a-library-d05b6bc35e66c6ef.yaml
index 4886f16..9cfce0d 100644
--- a/releasenotes/notes/add-volume-clients-as-a-library-d05b6bc35e66c6ef.yaml
+++ b/releasenotes/notes/add-volume-clients-as-a-library-d05b6bc35e66c6ef.yaml
@@ -6,6 +6,7 @@
     so the other projects can use these modules as stable libraries without
     any maintenance changes.
 
+    * backups_client
     * encryption_types_client (v1)
     * encryption_types_client (v2)
     * qos_clients (v1)
diff --git a/requirements.txt b/requirements.txt
index 0a5268c..9b47e77 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,7 +2,7 @@
 # of appearance. Changing the order has an impact on the overall integration
 # process, which may cause wedges in the gate later.
 pbr>=1.6 # Apache-2.0
-cliff!=1.16.0,!=1.17.0,>=1.15.0 # Apache-2.0
+cliff>=2.2.0 # Apache-2.0
 jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT
 testtools>=1.4.0 # MIT
 paramiko>=2.0 # LGPLv2.1+
@@ -16,11 +16,11 @@
 six>=1.9.0 # MIT
 fixtures>=3.0.0 # Apache-2.0/BSD
 testscenarios>=0.4 # Apache-2.0/BSD
-PyYAML>=3.1.0 # MIT
+PyYAML>=3.10.0 # MIT
 python-subunit>=0.0.18 # Apache-2.0/BSD
 stevedore>=1.16.0 # Apache-2.0
-PrettyTable<0.8,>=0.7 # BSD
-os-testr>=0.7.0 # Apache-2.0
+PrettyTable<0.8,>=0.7.1 # BSD
+os-testr>=0.8.0 # Apache-2.0
 urllib3>=1.15.1 # MIT
 debtcollector>=1.2.0 # Apache-2.0
 unittest2 # BSD
diff --git a/tempest/api/compute/admin/test_volume_swap.py b/tempest/api/compute/admin/test_volume_swap.py
new file mode 100644
index 0000000..f603abd
--- /dev/null
+++ b/tempest/api/compute/admin/test_volume_swap.py
@@ -0,0 +1,75 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.api.compute import base
+from tempest.common import waiters
+from tempest import config
+from tempest import test
+
+CONF = config.CONF
+
+
+class TestVolumeSwap(base.BaseV2ComputeAdminTest):
+    """The test suite for swapping of volume with admin user.
+
+    The following is the scenario outline:
+    1. Create a volume "volume1" with non-admin.
+    2. Create a volume "volume2" with non-admin.
+    3. Boot an instance "instance1" with non-admin.
+    4. Attach "volume1" to "instance1" with non-admin.
+    5. Swap volume from "volume1" to "volume2" as admin.
+    6. Check the swap volume is successful and "volume2"
+       is attached to "instance1" and "volume1" is in available state.
+    """
+
+    @classmethod
+    def skip_checks(cls):
+        super(TestVolumeSwap, cls).skip_checks()
+        if not CONF.compute_feature_enabled.swap_volume:
+            raise cls.skipException("Swapping volumes is not supported.")
+
+    @classmethod
+    def setup_clients(cls):
+        super(TestVolumeSwap, cls).setup_clients()
+        # We need the admin client for performing the update (swap) volume call
+        cls.servers_admin_client = cls.os_adm.servers_client
+
+    @test.idempotent_id('1769f00d-a693-4d67-a631-6a3496773813')
+    @test.services('volume')
+    def test_volume_swap(self):
+        # Create two volumes.
+        # NOTE(gmann): Volumes are created before server creation so that
+        # volumes cleanup can happen successfully irrespective of which volume
+        # is attached to server.
+        volume1 = self.create_volume()
+        volume2 = self.create_volume()
+        # Boot server
+        server = self.create_test_server(wait_until='ACTIVE')
+        # Attach "volume1" to server
+        self.attach_volume(server, volume1)
+        # Swap volume from "volume1" to "volume2"
+        self.servers_admin_client.update_attached_volume(
+            server['id'], volume1['id'], volumeId=volume2['id'])
+        waiters.wait_for_volume_status(self.volumes_client,
+                                       volume1['id'], 'available')
+        waiters.wait_for_volume_status(self.volumes_client,
+                                       volume2['id'], 'in-use')
+        self.addCleanup(self.servers_client.detach_volume,
+                        server['id'], volume2['id'])
+        # Verify "volume2" is attached to the server
+        vol_attachments = self.servers_client.list_volume_attachments(
+            server['id'])['volumeAttachments']
+        self.assertEqual(1, len(vol_attachments))
+        self.assertIn(volume2['id'], vol_attachments[0]['volumeId'])
+
+        # TODO(mriedem): Test swapping back from volume2 to volume1 after
+        # nova bug 1490236 is fixed.
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index 27afff3..a4578ae 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -120,6 +120,7 @@
         cls.images = []
         cls.security_groups = []
         cls.server_groups = []
+        cls.volumes = []
 
     @classmethod
     def resource_cleanup(cls):
@@ -127,6 +128,7 @@
         cls.clear_servers()
         cls.clear_security_groups()
         cls.clear_server_groups()
+        cls.clear_volumes()
         super(BaseV2ComputeTest, cls).resource_cleanup()
 
     @classmethod
@@ -370,6 +372,55 @@
         self.useFixture(api_microversion_fixture.APIMicroversionFixture(
             self.request_microversion))
 
+    @classmethod
+    def create_volume(cls):
+        """Create a volume and wait for it to become 'available'.
+
+        :returns: The available volume.
+        """
+        vol_name = data_utils.rand_name(cls.__name__ + '-volume')
+        volume = cls.volumes_client.create_volume(
+            size=CONF.volume.volume_size, display_name=vol_name)['volume']
+        cls.volumes.append(volume)
+        waiters.wait_for_volume_status(cls.volumes_client,
+                                       volume['id'], 'available')
+        return volume
+
+    @classmethod
+    def clear_volumes(cls):
+        LOG.debug('Clearing volumes: %s', ','.join(
+            volume['id'] for volume in cls.volumes))
+        for volume in cls.volumes:
+            try:
+                test_utils.call_and_ignore_notfound_exc(
+                    cls.volumes_client.delete_volume, volume['id'])
+            except Exception:
+                LOG.exception('Deleting volume %s failed', volume['id'])
+
+        for volume in cls.volumes:
+            try:
+                cls.volumes_client.wait_for_resource_deletion(volume['id'])
+            except Exception:
+                LOG.exception('Waiting for deletion of volume %s failed',
+                              volume['id'])
+
+    def attach_volume(self, server, volume):
+        """Attaches volume to server and waits for 'in-use' volume status."""
+        self.servers_client.attach_volume(
+            server['id'], volumeId=volume['id'])
+        # On teardown detach the volume and wait for it to be available. This
+        # is so we don't error out when trying to delete the volume during
+        # teardown.
+        self.addCleanup(waiters.wait_for_volume_status,
+                        self.volumes_client, volume['id'], 'available')
+        # Ignore 404s on detach in case the server is deleted or the volume
+        # is already detached.
+        self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+                        self.servers_client.detach_volume,
+                        server['id'], volume['id'])
+        waiters.wait_for_volume_status(self.volumes_client,
+                                       volume['id'], 'in-use')
+
 
 class BaseV2ComputeAdminTest(BaseV2ComputeTest):
     """Base test case class for Compute Admin API tests."""
diff --git a/tempest/api/image/admin/v2/test_images.py b/tempest/api/image/admin/v2/test_images.py
index c719b7a..9844a67 100644
--- a/tempest/api/image/admin/v2/test_images.py
+++ b/tempest/api/image/admin/v2/test_images.py
@@ -34,27 +34,26 @@
     def test_admin_deactivate_reactivate_image(self):
         # Create image by non-admin tenant
         image_name = data_utils.rand_name('image')
-        body = self.client.create_image(name=image_name,
-                                        container_format='bare',
-                                        disk_format='raw',
-                                        visibility='private')
-        image_id = body['id']
-        self.addCleanup(self.client.delete_image, image_id)
+        image = self.client.create_image(name=image_name,
+                                         container_format='bare',
+                                         disk_format='raw',
+                                         visibility='private')
+        self.addCleanup(self.client.delete_image, image['id'])
         # upload an image file
         content = data_utils.random_bytes()
         image_file = six.BytesIO(content)
-        self.client.store_image_file(image_id, image_file)
+        self.client.store_image_file(image['id'], image_file)
         # deactivate image
-        self.admin_client.deactivate_image(image_id)
-        body = self.client.show_image(image_id)
+        self.admin_client.deactivate_image(image['id'])
+        body = self.client.show_image(image['id'])
         self.assertEqual("deactivated", body['status'])
         # non-admin user unable to download deactivated image
         self.assertRaises(lib_exc.Forbidden, self.client.show_image_file,
-                          image_id)
+                          image['id'])
         # reactivate image
-        self.admin_client.reactivate_image(image_id)
-        body = self.client.show_image(image_id)
+        self.admin_client.reactivate_image(image['id'])
+        body = self.client.show_image(image['id'])
         self.assertEqual("active", body['status'])
         # non-admin user able to download image after reactivation by admin
-        body = self.client.show_image_file(image_id)
+        body = self.client.show_image_file(image['id'])
         self.assertEqual(content, body.data)
diff --git a/tempest/api/image/base.py b/tempest/api/image/base.py
index f74f97b..26b88b0 100644
--- a/tempest/api/image/base.py
+++ b/tempest/api/image/base.py
@@ -123,8 +123,7 @@
                                   disk_format='raw',
                                   is_public=False,
                                   data=image_file)
-        image_id = image['id']
-        return image_id
+        return image['id']
 
 
 class BaseV2ImageTest(BaseImageTest):
@@ -183,9 +182,8 @@
         image = self.client.create_image(name=name,
                                          container_format='bare',
                                          disk_format='raw')
-        image_id = image['id']
-        self.addCleanup(self.client.delete_image, image_id)
-        return image_id
+        self.addCleanup(self.client.delete_image, image['id'])
+        return image['id']
 
 
 class BaseV1ImageAdminTest(BaseImageTest):
diff --git a/tempest/api/image/v1/test_images.py b/tempest/api/image/v1/test_images.py
index 712b34b..695efb5 100644
--- a/tempest/api/image/v1/test_images.py
+++ b/tempest/api/image/v1/test_images.py
@@ -49,22 +49,21 @@
         # Register, then upload an image
         properties = {'prop1': 'val1'}
         container_format, disk_format = get_container_and_disk_format()
-        body = self.create_image(name='New Name',
-                                 container_format=container_format,
-                                 disk_format=disk_format,
-                                 is_public=False,
-                                 properties=properties)
-        self.assertIn('id', body)
-        image_id = body.get('id')
-        self.assertEqual('New Name', body.get('name'))
-        self.assertFalse(body.get('is_public'))
-        self.assertEqual('queued', body.get('status'))
+        image = self.create_image(name='New Name',
+                                  container_format=container_format,
+                                  disk_format=disk_format,
+                                  is_public=False,
+                                  properties=properties)
+        self.assertIn('id', image)
+        self.assertEqual('New Name', image.get('name'))
+        self.assertFalse(image.get('is_public'))
+        self.assertEqual('queued', image.get('status'))
         for key, val in properties.items():
-            self.assertEqual(val, body.get('properties')[key])
+            self.assertEqual(val, image.get('properties')[key])
 
         # Now try uploading an image file
         image_file = six.BytesIO(data_utils.random_bytes())
-        body = self.client.update_image(image_id, data=image_file)['image']
+        body = self.client.update_image(image['id'], data=image_file)['image']
         self.assertIn('size', body)
         self.assertEqual(1024, body.get('size'))
 
@@ -89,16 +88,15 @@
     @test.idempotent_id('6d0e13a7-515b-460c-b91f-9f4793f09816')
     def test_register_http_image(self):
         container_format, disk_format = get_container_and_disk_format()
-        body = self.create_image(name='New Http Image',
-                                 container_format=container_format,
-                                 disk_format=disk_format, is_public=False,
-                                 copy_from=CONF.image.http_image)
-        self.assertIn('id', body)
-        image_id = body.get('id')
-        self.assertEqual('New Http Image', body.get('name'))
-        self.assertFalse(body.get('is_public'))
-        waiters.wait_for_image_status(self.client, image_id, 'active')
-        self.client.show_image(image_id)
+        image = self.create_image(name='New Http Image',
+                                  container_format=container_format,
+                                  disk_format=disk_format, is_public=False,
+                                  copy_from=CONF.image.http_image)
+        self.assertIn('id', image)
+        self.assertEqual('New Http Image', image.get('name'))
+        self.assertFalse(image.get('is_public'))
+        waiters.wait_for_image_status(self.client, image['id'], 'active')
+        self.client.show_image(image['id'])
 
     @test.idempotent_id('05b19d55-140c-40d0-b36b-fafd774d421b')
     def test_register_image_with_min_ram(self):
@@ -188,8 +186,7 @@
                                  disk_format=disk_format,
                                  is_public=False,
                                  location=location)
-        image_id = image['id']
-        return image_id
+        return image['id']
 
     @classmethod
     def _create_standard_image(cls, name, container_format,
@@ -205,8 +202,7 @@
                                  container_format=container_format,
                                  disk_format=disk_format,
                                  is_public=False, data=image_file)
-        image_id = image['id']
-        return image_id
+        return image['id']
 
     @test.idempotent_id('246178ab-3b33-4212-9a4b-a7fe8261794d')
     def test_index_no_params(self):
@@ -301,8 +297,7 @@
                                  disk_format=disk_format,
                                  is_public=False, data=image_file,
                                  properties={'key1': 'value1'})
-        image_id = image['id']
-        return image_id
+        return image['id']
 
     @test.idempotent_id('01752c1c-0275-4de3-9e5b-876e44541928')
     def test_list_image_metadata(self):
diff --git a/tempest/api/image/v2/test_images.py b/tempest/api/image/v2/test_images.py
index f363d34..aff8a78 100644
--- a/tempest/api/image/v2/test_images.py
+++ b/tempest/api/image/v2/test_images.py
@@ -44,35 +44,34 @@
         image_name = data_utils.rand_name('image')
         container_format = CONF.image.container_formats[0]
         disk_format = CONF.image.disk_formats[0]
-        body = self.create_image(name=image_name,
-                                 container_format=container_format,
-                                 disk_format=disk_format,
-                                 visibility='private',
-                                 ramdisk_id=uuid)
-        self.assertIn('id', body)
-        image_id = body.get('id')
-        self.assertIn('name', body)
-        self.assertEqual(image_name, body['name'])
-        self.assertIn('visibility', body)
-        self.assertEqual('private', body['visibility'])
-        self.assertIn('status', body)
-        self.assertEqual('queued', body['status'])
+        image = self.create_image(name=image_name,
+                                  container_format=container_format,
+                                  disk_format=disk_format,
+                                  visibility='private',
+                                  ramdisk_id=uuid)
+        self.assertIn('id', image)
+        self.assertIn('name', image)
+        self.assertEqual(image_name, image['name'])
+        self.assertIn('visibility', image)
+        self.assertEqual('private', image['visibility'])
+        self.assertIn('status', image)
+        self.assertEqual('queued', image['status'])
 
         # Now try uploading an image file
         file_content = data_utils.random_bytes()
         image_file = six.BytesIO(file_content)
-        self.client.store_image_file(image_id, image_file)
+        self.client.store_image_file(image['id'], image_file)
 
         # Now try to get image details
-        body = self.client.show_image(image_id)
-        self.assertEqual(image_id, body['id'])
+        body = self.client.show_image(image['id'])
+        self.assertEqual(image['id'], body['id'])
         self.assertEqual(image_name, body['name'])
         self.assertEqual(uuid, body['ramdisk_id'])
         self.assertIn('size', body)
         self.assertEqual(1024, body.get('size'))
 
         # Now try get image file
-        body = self.client.show_image_file(image_id)
+        body = self.client.show_image_file(image['id'])
         self.assertEqual(file_content, body.data)
 
     @test.attr(type='smoke')
@@ -84,20 +83,18 @@
         image_name = data_utils.rand_name('image')
         container_format = CONF.image.container_formats[0]
         disk_format = CONF.image.disk_formats[0]
-        body = self.client.create_image(name=image_name,
-                                        container_format=container_format,
-                                        disk_format=disk_format,
-                                        visibility='private')
-        image_id = body['id']
-
+        image = self.client.create_image(name=image_name,
+                                         container_format=container_format,
+                                         disk_format=disk_format,
+                                         visibility='private')
         # Delete Image
-        self.client.delete_image(image_id)
-        self.client.wait_for_resource_deletion(image_id)
+        self.client.delete_image(image['id'])
+        self.client.wait_for_resource_deletion(image['id'])
 
         # Verifying deletion
         images = self.client.list_images()['images']
         images_id = [item['id'] for item in images]
-        self.assertNotIn(image_id, images_id)
+        self.assertNotIn(image['id'], images_id)
 
     @test.attr(type='smoke')
     @test.idempotent_id('f66891a7-a35c-41a8-b590-a065c2a1caa6')
@@ -108,27 +105,26 @@
         image_name = data_utils.rand_name('image')
         container_format = CONF.image.container_formats[0]
         disk_format = CONF.image.disk_formats[0]
-        body = self.client.create_image(name=image_name,
-                                        container_format=container_format,
-                                        disk_format=disk_format,
-                                        visibility='private')
-        self.addCleanup(self.client.delete_image, body['id'])
-        self.assertEqual('queued', body['status'])
-        image_id = body['id']
+        image = self.client.create_image(name=image_name,
+                                         container_format=container_format,
+                                         disk_format=disk_format,
+                                         visibility='private')
+        self.addCleanup(self.client.delete_image, image['id'])
+        self.assertEqual('queued', image['status'])
 
         # Now try uploading an image file
         image_file = six.BytesIO(data_utils.random_bytes())
-        self.client.store_image_file(image_id, image_file)
+        self.client.store_image_file(image['id'], image_file)
 
         # Update Image
         new_image_name = data_utils.rand_name('new-image')
-        body = self.client.update_image(image_id, [
+        body = self.client.update_image(image['id'], [
             dict(replace='/name', value=new_image_name)])
 
         # Verifying updating
 
-        body = self.client.show_image(image_id)
-        self.assertEqual(image_id, body['id'])
+        body = self.client.show_image(image['id'])
+        self.assertEqual(image['id'], body['id'])
         self.assertEqual(new_image_name, body['name'])
 
 
@@ -162,14 +158,13 @@
         size = random.randint(1024, 4096)
         image_file = six.BytesIO(data_utils.random_bytes(size))
         name = data_utils.rand_name('image')
-        body = cls.create_image(name=name,
-                                container_format=container_format,
-                                disk_format=disk_format,
-                                visibility='private')
-        image_id = body['id']
-        cls.client.store_image_file(image_id, data=image_file)
+        image = cls.create_image(name=name,
+                                 container_format=container_format,
+                                 disk_format=disk_format,
+                                 visibility='private')
+        cls.client.store_image_file(image['id'], data=image_file)
 
-        return image_id
+        return image['id']
 
     def _list_by_param_value_and_assert(self, params):
         """Perform list action with given params and validates result."""
diff --git a/tempest/api/image/v2/test_images_negative.py b/tempest/api/image/v2/test_images_negative.py
index f60fb0c..cd1bca0 100644
--- a/tempest/api/image/v2/test_images_negative.py
+++ b/tempest/api/image/v2/test_images_negative.py
@@ -53,19 +53,19 @@
     def test_get_delete_deleted_image(self):
         # get and delete the deleted image
         # create and delete image
-        body = self.client.create_image(name='test',
-                                        container_format='bare',
-                                        disk_format='raw')
-        image_id = body['id']
-        self.client.delete_image(image_id)
-        self.client.wait_for_resource_deletion(image_id)
+        image = self.client.create_image(name='test',
+                                         container_format='bare',
+                                         disk_format='raw')
+        self.client.delete_image(image['id'])
+        self.client.wait_for_resource_deletion(image['id'])
 
         # get the deleted image
-        self.assertRaises(lib_exc.NotFound, self.client.show_image, image_id)
+        self.assertRaises(lib_exc.NotFound,
+                          self.client.show_image, image['id'])
 
         # delete the deleted image
         self.assertRaises(lib_exc.NotFound, self.client.delete_image,
-                          image_id)
+                          image['id'])
 
     @test.attr(type=['negative'])
     @test.idempotent_id('6fe40f1c-57bd-4918-89cc-8500f850f3de')
diff --git a/tempest/api/image/v2/test_images_tags.py b/tempest/api/image/v2/test_images_tags.py
index 42a4b87..03f29bd 100644
--- a/tempest/api/image/v2/test_images_tags.py
+++ b/tempest/api/image/v2/test_images_tags.py
@@ -21,19 +21,18 @@
 
     @test.idempotent_id('10407036-6059-4f95-a2cd-cbbbee7ed329')
     def test_update_delete_tags_for_image(self):
-        body = self.create_image(container_format='bare',
-                                 disk_format='raw',
-                                 visibility='private')
-        image_id = body['id']
+        image = self.create_image(container_format='bare',
+                                  disk_format='raw',
+                                  visibility='private')
         tag = data_utils.rand_name('tag')
-        self.addCleanup(self.client.delete_image, image_id)
+        self.addCleanup(self.client.delete_image, image['id'])
 
         # Creating image tag and verify it.
-        self.client.add_image_tag(image_id, tag)
-        body = self.client.show_image(image_id)
+        self.client.add_image_tag(image['id'], tag)
+        body = self.client.show_image(image['id'])
         self.assertIn(tag, body['tags'])
 
         # Deleting image tag and verify it.
-        self.client.delete_image_tag(image_id, tag)
-        body = self.client.show_image(image_id)
+        self.client.delete_image_tag(image['id'], tag)
+        body = self.client.show_image(image['id'])
         self.assertNotIn(tag, body['tags'])
diff --git a/tempest/api/image/v2/test_images_tags_negative.py b/tempest/api/image/v2/test_images_tags_negative.py
index dd5650f..af4ffcf 100644
--- a/tempest/api/image/v2/test_images_tags_negative.py
+++ b/tempest/api/image/v2/test_images_tags_negative.py
@@ -33,12 +33,11 @@
     @test.idempotent_id('39c023a2-325a-433a-9eea-649bf1414b19')
     def test_delete_non_existing_tag(self):
         # Delete non existing tag.
-        body = self.create_image(container_format='bare',
-                                 disk_format='raw',
-                                 visibility='private'
-                                 )
-        image_id = body['id']
+        image = self.create_image(container_format='bare',
+                                  disk_format='raw',
+                                  visibility='private'
+                                  )
         tag = data_utils.rand_name('non-exist-tag')
-        self.addCleanup(self.client.delete_image, image_id)
+        self.addCleanup(self.client.delete_image, image['id'])
         self.assertRaises(lib_exc.NotFound, self.client.delete_image_tag,
-                          image_id, tag)
+                          image['id'], tag)
diff --git a/tempest/common/compute.py b/tempest/common/compute.py
index 8e9f0b0..318eb10 100644
--- a/tempest/common/compute.py
+++ b/tempest/common/compute.py
@@ -129,7 +129,6 @@
                                                 **kwargs)
 
     # handle the case of multiple servers
-    servers = []
     if multiple_create_request:
         # Get servers created which name match with name param.
         body_servers = clients.servers_client.list_servers()
diff --git a/tempest/config.py b/tempest/config.py
index b6fca7e..eeafea6 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -414,7 +414,10 @@
                      "list indicates all filters are disabled. The full "
                      "available list of filters is in nova.conf: "
                      "DEFAULT.scheduler_available_filters"),
-
+    cfg.BoolOpt('swap_volume',
+                default=False,
+                help='Does the test environment support in-place swapping of '
+                     'volumes attached to a server instance?'),
 ]
 
 
diff --git a/tempest/exceptions.py b/tempest/exceptions.py
index 272f6e3..da32693 100644
--- a/tempest/exceptions.py
+++ b/tempest/exceptions.py
@@ -53,10 +53,6 @@
     message = "Snapshot %(snapshot_id)s failed to build and is in ERROR status"
 
 
-class VolumeBackupException(exceptions.TempestException):
-    message = "Volume backup %(backup_id)s failed and is in ERROR status"
-
-
 class StackBuildErrorException(exceptions.TempestException):
     message = ("Stack %(stack_identifier)s is in %(stack_status)s status "
                "due to '%(stack_status_reason)s'")
diff --git a/tempest/lib/exceptions.py b/tempest/lib/exceptions.py
index e3f25e6..a5c6b1b 100644
--- a/tempest/lib/exceptions.py
+++ b/tempest/lib/exceptions.py
@@ -239,3 +239,7 @@
 
 class PluginRegistrationException(TempestException):
     message = "Error registering plugin %(name)s: %(detailed_error)s"
+
+
+class VolumeBackupException(TempestException):
+    message = "Volume backup %(backup_id)s failed and is in ERROR status"
diff --git a/tempest/services/volume/base/base_backups_client.py b/tempest/lib/services/volume/v1/backups_client.py
similarity index 96%
rename from tempest/services/volume/base/base_backups_client.py
rename to tempest/lib/services/volume/v1/backups_client.py
index 1b35feb..2728c67 100644
--- a/tempest/services/volume/base/base_backups_client.py
+++ b/tempest/lib/services/volume/v1/backups_client.py
@@ -19,8 +19,9 @@
 from tempest.lib import exceptions as lib_exc
 
 
-class BaseBackupsClient(rest_client.RestClient):
-    """Client class to send CRUD Volume backup API requests"""
+class BackupsClient(rest_client.RestClient):
+    """Volume V1 Backups client"""
+    api_version = "v1"
 
     def create_backup(self, **kwargs):
         """Creates a backup of volume.
diff --git a/tempest/services/volume/base/base_backups_client.py b/tempest/lib/services/volume/v2/backups_client.py
similarity index 96%
copy from tempest/services/volume/base/base_backups_client.py
copy to tempest/lib/services/volume/v2/backups_client.py
index 1b35feb..61f865d 100644
--- a/tempest/services/volume/base/base_backups_client.py
+++ b/tempest/lib/services/volume/v2/backups_client.py
@@ -19,8 +19,9 @@
 from tempest.lib import exceptions as lib_exc
 
 
-class BaseBackupsClient(rest_client.RestClient):
-    """Client class to send CRUD Volume backup API requests"""
+class BackupsClient(rest_client.RestClient):
+    """Volume V2 Backups client"""
+    api_version = "v2"
 
     def create_backup(self, **kwargs):
         """Creates a backup of volume.
diff --git a/tempest/services/volume/v1/__init__.py b/tempest/services/volume/v1/__init__.py
index e386faf..376ab72 100644
--- a/tempest/services/volume/v1/__init__.py
+++ b/tempest/services/volume/v1/__init__.py
@@ -14,6 +14,7 @@
 
 from tempest.lib.services.volume.v1.availability_zone_client import \
     AvailabilityZoneClient
+from tempest.lib.services.volume.v1.backups_client import BackupsClient
 from tempest.lib.services.volume.v1.encryption_types_client import \
     EncryptionTypesClient
 from tempest.lib.services.volume.v1.extensions_client import ExtensionsClient
@@ -23,7 +24,6 @@
 from tempest.lib.services.volume.v1.services_client import ServicesClient
 from tempest.lib.services.volume.v1.snapshots_client import SnapshotsClient
 from tempest.lib.services.volume.v1.types_client import TypesClient
-from tempest.services.volume.v1.json.backups_client import BackupsClient
 from tempest.services.volume.v1.json.volumes_client import VolumesClient
 
 __all__ = ['AvailabilityZoneClient', 'EncryptionTypesClient',
diff --git a/tempest/services/volume/v1/json/backups_client.py b/tempest/services/volume/v1/json/backups_client.py
deleted file mode 100644
index ac6db6a..0000000
--- a/tempest/services/volume/v1/json/backups_client.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Copyright 2014 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.services.volume.base import base_backups_client
-
-
-class BackupsClient(base_backups_client.BaseBackupsClient):
-    """Volume V1 Backups client"""
diff --git a/tempest/services/volume/v2/__init__.py b/tempest/services/volume/v2/__init__.py
index fcc34f9..5774977 100644
--- a/tempest/services/volume/v2/__init__.py
+++ b/tempest/services/volume/v2/__init__.py
@@ -14,6 +14,7 @@
 
 from tempest.lib.services.volume.v2.availability_zone_client import \
     AvailabilityZoneClient
+from tempest.lib.services.volume.v2.backups_client import BackupsClient
 from tempest.lib.services.volume.v2.encryption_types_client import \
     EncryptionTypesClient
 from tempest.lib.services.volume.v2.extensions_client import ExtensionsClient
@@ -23,10 +24,9 @@
 from tempest.lib.services.volume.v2.services_client import ServicesClient
 from tempest.lib.services.volume.v2.snapshots_client import SnapshotsClient
 from tempest.lib.services.volume.v2.types_client import TypesClient
-from tempest.services.volume.v2.json.backups_client import BackupsClient
 from tempest.services.volume.v2.json.volumes_client import VolumesClient
 
-__all__ = ['AvailabilityZoneClient', 'EncryptionTypesClient',
-           'ExtensionsClient', 'HostsClient', 'QosSpecsClient',
-           'QuotasClient', 'ServicesClient', 'SnapshotsClient',
-           'TypesClient', 'BackupsClient', 'VolumesClient', ]
+__all__ = ['AvailabilityZoneClient', 'BackupsClient', 'EncryptionTypesClient',
+           'ExtensionsClient', 'HostsClient', 'QosSpecsClient', 'QuotasClient',
+           'ServicesClient', 'SnapshotsClient', 'TypesClient',
+           'VolumesClient', ]
diff --git a/tempest/services/volume/v2/json/backups_client.py b/tempest/services/volume/v2/json/backups_client.py
deleted file mode 100644
index 78bab82..0000000
--- a/tempest/services/volume/v2/json/backups_client.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Copyright 2014 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.services.volume.base import base_backups_client
-
-
-class BackupsClient(base_backups_client.BaseBackupsClient):
-    """Client class to send CRUD Volume V2 API requests"""
-    api_version = "v2"
diff --git a/tox.ini b/tox.ini
index 7096e60..02eef78 100644
--- a/tox.ini
+++ b/tox.ini
@@ -26,7 +26,7 @@
     -r{toxinidir}/test-requirements.txt
 commands =
     find . -type f -name "*.pyc" -delete
-    bash tools/pretty_tox.sh '{posargs}'
+    ostestr {posargs}
 
 [testenv:genconfig]
 commands = oslo-config-generator --config-file tempest/cmd/config-generator.tempest.conf