Merge "Fix victoria releasenote to align yaml format"
diff --git a/releasenotes/notes/add-keystone-v3-ec2-tests-d959b7d36f0bd7fc.yaml b/releasenotes/notes/add-keystone-v3-ec2-tests-d959b7d36f0bd7fc.yaml
new file mode 100644
index 0000000..ab8d748
--- /dev/null
+++ b/releasenotes/notes/add-keystone-v3-ec2-tests-d959b7d36f0bd7fc.yaml
@@ -0,0 +1,5 @@
+---
+features:
+  - |
+    Added missing clients and tests for keystone's v3 EC2 API which already
+    existed for keystone v2.
diff --git a/tempest/api/identity/v3/test_ec2_credentials.py b/tempest/api/identity/v3/test_ec2_credentials.py
new file mode 100644
index 0000000..a2cbc4a
--- /dev/null
+++ b/tempest/api/identity/v3/test_ec2_credentials.py
@@ -0,0 +1,113 @@
+# Copyright 2020 SUSE LLC
+# 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.identity import base
+from tempest.common import utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+
+class EC2CredentialsTest(base.BaseIdentityV3Test):
+
+    @classmethod
+    def skip_checks(cls):
+        super(EC2CredentialsTest, cls).skip_checks()
+        if not utils.is_extension_enabled('OS-EC2', 'identity'):
+            msg = "OS-EC2 identity extension not enabled."
+            raise cls.skipException(msg)
+
+    @classmethod
+    def resource_setup(cls):
+        super(EC2CredentialsTest, cls).resource_setup()
+        cls.creds = cls.os_primary.credentials
+
+    @decorators.idempotent_id('b0f55a29-54e5-4166-999d-712347e0c920')
+    def test_create_ec2_credential(self):
+        """Create user ec2 credential."""
+        resp = self.non_admin_users_client.create_user_ec2_credential(
+            self.creds.user_id,
+            tenant_id=self.creds.tenant_id)["credential"]
+        access = resp['access']
+        self.addCleanup(
+            self.non_admin_users_client.delete_user_ec2_credential,
+            self.creds.user_id, access)
+        self.assertNotEmpty(resp['access'])
+        self.assertNotEmpty(resp['secret'])
+        self.assertEqual(self.creds.user_id, resp['user_id'])
+        self.assertEqual(self.creds.tenant_id, resp['tenant_id'])
+
+    @decorators.idempotent_id('897813f0-160c-4fdc-aabc-24ee635ce4a9')
+    def test_list_ec2_credentials(self):
+        """Get the list of user ec2 credentials."""
+        created_creds = []
+        # create first ec2 credentials
+        creds1 = self.non_admin_users_client.create_user_ec2_credential(
+            self.creds.user_id,
+            tenant_id=self.creds.tenant_id)["credential"]
+        created_creds.append(creds1['access'])
+        self.addCleanup(
+            self.non_admin_users_client.delete_user_ec2_credential,
+            self.creds.user_id, creds1['access'])
+
+        # create second ec2 credentials
+        creds2 = self.non_admin_users_client.create_user_ec2_credential(
+            self.creds.user_id,
+            tenant_id=self.creds.tenant_id)["credential"]
+        created_creds.append(creds2['access'])
+        self.addCleanup(
+            self.non_admin_users_client.delete_user_ec2_credential,
+            self.creds.user_id, creds2['access'])
+
+        # get the list of user ec2 credentials
+        resp = self.non_admin_users_client.list_user_ec2_credentials(
+            self.creds.user_id)["credentials"]
+        fetched_creds = [cred['access'] for cred in resp]
+        # created credentials should be in a fetched list
+        missing = [cred for cred in created_creds
+                   if cred not in fetched_creds]
+        self.assertEmpty(missing,
+                         "Failed to find ec2_credentials %s in fetched list" %
+                         ', '.join(cred for cred in missing))
+
+    @decorators.idempotent_id('8b8d1010-5958-48df-a6cd-5e3df72e6bcf')
+    def test_show_ec2_credential(self):
+        """Get the definite user ec2 credential."""
+        resp = self.non_admin_users_client.create_user_ec2_credential(
+            self.creds.user_id,
+            tenant_id=self.creds.tenant_id)["credential"]
+        self.addCleanup(
+            self.non_admin_users_client.delete_user_ec2_credential,
+            self.creds.user_id, resp['access'])
+
+        ec2_creds = self.non_admin_users_client.show_user_ec2_credential(
+            self.creds.user_id, resp['access']
+        )["credential"]
+        for key in ['access', 'secret', 'user_id', 'tenant_id']:
+            self.assertEqual(ec2_creds[key], resp[key])
+
+    @decorators.idempotent_id('9408d61b-8be0-4a8d-9b85-14f61edb456b')
+    def test_delete_ec2_credential(self):
+        """Delete user ec2 credential."""
+        resp = self.non_admin_users_client.create_user_ec2_credential(
+            self.creds.user_id,
+            tenant_id=self.creds.tenant_id)["credential"]
+        access = resp['access']
+        self.non_admin_users_client.delete_user_ec2_credential(
+            self.creds.user_id, access)
+        self.assertRaises(
+            lib_exc.NotFound,
+            self.non_admin_users_client.show_user_ec2_credential,
+            self.creds.user_id,
+            access)
diff --git a/tempest/api/image/base.py b/tempest/api/image/base.py
index ae7b3e4..d3dc19a 100644
--- a/tempest/api/image/base.py
+++ b/tempest/api/image/base.py
@@ -18,6 +18,7 @@
 from tempest import config
 from tempest.lib.common.utils import data_utils
 from tempest.lib.common.utils import test_utils
+from tempest.lib import exceptions
 import tempest.test
 
 CONF = config.CONF
@@ -155,6 +156,15 @@
                         namespace_name)
         return namespace
 
+    @classmethod
+    def get_available_stores(cls):
+        stores = []
+        try:
+            stores = cls.client.info_stores()['stores']
+        except exceptions.NotFound:
+            pass
+        return stores
+
 
 class BaseV2MemberImageTest(BaseV2ImageTest):
 
diff --git a/tempest/api/image/v2/test_images.py b/tempest/api/image/v2/test_images.py
index c1a7211..28299a4 100644
--- a/tempest/api/image/v2/test_images.py
+++ b/tempest/api/image/v2/test_images.py
@@ -20,6 +20,7 @@
 
 from oslo_log import log as logging
 from tempest.api.image import base
+from tempest.common import waiters
 from tempest import config
 from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
@@ -113,6 +114,95 @@
         self.client.wait_for_resource_activation(image['id'])
 
 
+class MultiStoresImportImagesTest(base.BaseV2ImageTest):
+    """Test importing image in multiple stores"""
+    @classmethod
+    def skip_checks(cls):
+        super(MultiStoresImportImagesTest, cls).skip_checks()
+        if not CONF.image_feature_enabled.import_image:
+            skip_msg = (
+                "%s skipped as image import is not available" % cls.__name__)
+            raise cls.skipException(skip_msg)
+
+    @classmethod
+    def resource_setup(cls):
+        super(MultiStoresImportImagesTest, cls).resource_setup()
+        cls.available_import_methods = cls.client.info_import()[
+            'import-methods']['value']
+        if not cls.available_import_methods:
+            raise cls.skipException('Server does not support '
+                                    'any import method')
+
+        # NOTE(pdeore): Skip if glance-direct import method and mutlistore
+        # are not enabled/configured, or only one store is configured in
+        # multiple stores setup.
+        cls.available_stores = cls.get_available_stores()
+        if ('glance-direct' not in cls.available_import_methods or
+                not len(cls.available_stores) > 1):
+            raise cls.skipException(
+                'Either glance-direct import method not present in %s or '
+                'None or only one store is '
+                'configured %s' % (cls.available_import_methods,
+                                   cls.available_stores))
+
+    def _create_and_stage_image(self, all_stores=False):
+        """Create Image & stage image file for glance-direct import method."""
+        image_name = data_utils.rand_name('test-image')
+        container_format = CONF.image.container_formats[0]
+        disk_format = CONF.image.disk_formats[0]
+        image = self.create_image(name=image_name,
+                                  container_format=container_format,
+                                  disk_format=disk_format,
+                                  visibility='private')
+        self.assertEqual('queued', image['status'])
+
+        self.client.stage_image_file(
+            image['id'],
+            six.BytesIO(data_utils.random_bytes(10485760)))
+        # Check image status is 'uploading'
+        body = self.client.show_image(image['id'])
+        self.assertEqual(image['id'], body['id'])
+        self.assertEqual('uploading', body['status'])
+
+        if all_stores:
+            stores_list = ','.join([store['id']
+                                    for store in self.available_stores])
+        else:
+            stores = [store['id'] for store in self.available_stores]
+            stores_list = stores[::len(stores) - 1]
+
+        return body, stores_list
+
+    @decorators.idempotent_id('bf04ff00-3182-47cb-833a-f1c6767b47fd')
+    def test_glance_direct_import_image_to_all_stores(self):
+        """Test image is imported in all available stores
+
+        Create image, import image to all available stores using glance-direct
+        import method and verify that import succeeded.
+        """
+        image, stores = self._create_and_stage_image(all_stores=True)
+
+        self.client.image_import(
+            image['id'], method='glance-direct', all_stores=True)
+
+        waiters.wait_for_image_imported_to_stores(self.client,
+                                                  image['id'], stores)
+
+    @decorators.idempotent_id('82fb131a-dd2b-11ea-aec7-340286b6c574')
+    def test_glance_direct_import_image_to_specific_stores(self):
+        """Test image is imported in all available stores
+
+        Create image, import image to specified store(s) using glance-direct
+        import method and verify that import succeeded.
+        """
+        image, stores = self._create_and_stage_image()
+        self.client.image_import(image['id'], method='glance-direct',
+                                 stores=stores)
+
+        waiters.wait_for_image_imported_to_stores(self.client, image['id'],
+                                                  (','.join(stores)))
+
+
 class BasicOperationsImagesTest(base.BaseV2ImageTest):
     """Here we test the basic operations of images"""
 
diff --git a/tempest/api/object_storage/test_account_services.py b/tempest/api/object_storage/test_account_services.py
index c5c30e3..da8ad66 100644
--- a/tempest/api/object_storage/test_account_services.py
+++ b/tempest/api/object_storage/test_account_services.py
@@ -28,6 +28,7 @@
 
 
 class AccountTest(base.BaseObjectTest):
+    """Test account metadata and containers"""
 
     credentials = [['operator', CONF.object_storage.operator_role],
                    ['operator_alt', CONF.object_storage.operator_role]]
@@ -54,7 +55,7 @@
     @decorators.attr(type='smoke')
     @decorators.idempotent_id('3499406a-ae53-4f8c-b43a-133d4dc6fe3f')
     def test_list_containers(self):
-        # list of all containers should not be empty
+        """Test listing containers"""
         resp, container_list = self.account_client.list_account_containers()
         self.assertHeaders(resp, 'Account', 'GET')
 
@@ -66,11 +67,10 @@
 
     @decorators.idempotent_id('884ec421-fbad-4fcc-916b-0580f2699565')
     def test_list_no_containers(self):
-        # List request to empty account
+        """Test listing containers for an account without container"""
 
         # To test listing no containers, create new user other than
         # the base user of this instance.
-
         resp, container_list = \
             self.os_operator.account_client.list_account_containers()
 
@@ -103,7 +103,7 @@
 
     @decorators.idempotent_id('1c7efa35-e8a2-4b0b-b5ff-862c7fd83704')
     def test_list_containers_with_format_json(self):
-        # list containers setting format parameter to 'json'
+        """Test listing containers setting format parameter to 'json'"""
         params = {'format': 'json'}
         resp, container_list = self.account_client.list_account_containers(
             params=params)
@@ -115,7 +115,7 @@
 
     @decorators.idempotent_id('4477b609-1ca6-4d4b-b25d-ad3f01086089')
     def test_list_containers_with_format_xml(self):
-        # list containers setting format parameter to 'xml'
+        """Test listing containers setting format parameter to 'xml'"""
         params = {'format': 'xml'}
         resp, container_list = self.account_client.list_account_containers(
             params=params)
@@ -133,13 +133,18 @@
         not CONF.object_storage_feature_enabled.discoverability,
         'Discoverability function is disabled')
     def test_list_extensions(self):
+        """Test listing capabilities"""
         resp = self.capabilities_client.list_capabilities()
 
         self.assertThat(resp, custom_matchers.AreAllWellFormatted())
 
     @decorators.idempotent_id('5cfa4ab2-4373-48dd-a41f-a532b12b08b2')
     def test_list_containers_with_limit(self):
-        # list containers one of them, half of them then all of them
+        """Test listing containers with limit parameter
+
+        Listing containers limited to one of them, half of them, and then all
+        of them.
+        """
         for limit in (1, self.containers_count // 2,
                       self.containers_count):
             params = {'limit': limit}
@@ -151,10 +156,11 @@
 
     @decorators.idempotent_id('638f876d-6a43-482a-bbb3-0840bca101c6')
     def test_list_containers_with_marker(self):
-        # list containers using marker param
-        # first expect to get 0 container as we specified last
-        # the container as marker
-        # second expect to get the bottom half of the containers
+        """Test listing containers with marker parameter
+
+        First expect to get 0 container as we specified the last container
+        as marker, second expect to get the bottom half of the containers.
+        """
         params = {'marker': self.containers[-1]}
         resp, container_list = \
             self.account_client.list_account_containers(params=params)
@@ -172,10 +178,11 @@
 
     @decorators.idempotent_id('5ca164e4-7bde-43fa-bafb-913b53b9e786')
     def test_list_containers_with_end_marker(self):
-        # list containers using end_marker param
-        # first expect to get 0 container as we specified first container as
-        # end_marker
-        # second expect to get the top half of the containers
+        """Test listing containers with end_marker parameter
+
+        First expect to get 0 container as we specified first container as
+        end_marker, second expect to get the top half of the containers
+        """
         params = {'end_marker': self.containers[0]}
         resp, container_list = \
             self.account_client.list_account_containers(params=params)
@@ -190,7 +197,12 @@
 
     @decorators.idempotent_id('ac8502c2-d4e4-4f68-85a6-40befea2ef5e')
     def test_list_containers_with_marker_and_end_marker(self):
-        # list containers combining marker and end_marker param
+        """Test listing containers with marker and end_marker parameter
+
+        If we use the first container as marker, and the last container as
+        end_marker, then we should get all containers excluding the first one
+        and the last one.
+        """
         params = {'marker': self.containers[0],
                   'end_marker': self.containers[self.containers_count - 1]}
         resp, container_list = self.account_client.list_account_containers(
@@ -200,8 +212,10 @@
 
     @decorators.idempotent_id('f7064ae8-dbcc-48da-b594-82feef6ea5af')
     def test_list_containers_with_limit_and_marker(self):
-        # list containers combining marker and limit param
-        # result are always limitated by the limit whatever the marker
+        """Test listing containers combining marker and limit parameter
+
+        Result are always limited by the limit whatever the marker.
+        """
         for marker in random.choice(self.containers):
             limit = random.randint(0, self.containers_count - 1)
             params = {'marker': marker,
@@ -215,6 +229,10 @@
 
     @decorators.idempotent_id('888a3f0e-7214-4806-8e50-5e0c9a69bb5e')
     def test_list_containers_with_limit_and_end_marker(self):
+        """Test listing containers combining end_marker and limit parameter
+
+        Result are always limited by the limit whatever the end_marker.
+        """
         # list containers combining limit and end_marker param
         limit = random.randint(1, self.containers_count)
         params = {'limit': limit,
@@ -227,7 +245,11 @@
 
     @decorators.idempotent_id('8cf98d9c-e3a0-4e44-971b-c87656fdddbd')
     def test_list_containers_with_limit_and_marker_and_end_marker(self):
-        # list containers combining limit, marker and end_marker param
+        """Test listing containers combining marker and end_marker and limit
+
+        Result are always limited by the limit whatever the marker and the
+        end_marker.
+        """
         limit = random.randint(1, self.containers_count)
         params = {'limit': limit,
                   'marker': self.containers[0],
@@ -240,7 +262,7 @@
 
     @decorators.idempotent_id('365e6fc7-1cfe-463b-a37c-8bd08d47b6aa')
     def test_list_containers_with_prefix(self):
-        # list containers that have a name that starts with a prefix
+        """Test listing containers that have a name starting with a prefix"""
         prefix = 'tempest-a'
         params = {'prefix': prefix}
         resp, container_list = self.account_client.list_account_containers(
@@ -252,7 +274,7 @@
 
     @decorators.idempotent_id('b1811cff-d1ed-4c15-a52e-efd8de41cf34')
     def test_list_containers_reverse_order(self):
-        # list containers in reverse order
+        """Test listing containers in reverse order"""
         _, orig_container_list = self.account_client.list_account_containers()
 
         params = {'reverse': True}
@@ -265,8 +287,7 @@
     @decorators.attr(type='smoke')
     @decorators.idempotent_id('4894c312-6056-4587-8d6f-86ffbf861f80')
     def test_list_account_metadata(self):
-        # list all account metadata
-
+        """Test listing account metadata"""
         # set metadata to account
         metadata = {'test-account-meta1': 'Meta1',
                     'test-account-meta2': 'Meta2'}
@@ -282,14 +303,14 @@
 
     @decorators.idempotent_id('b904c2e3-24c2-4dba-ad7d-04e90a761be5')
     def test_list_no_account_metadata(self):
-        # list no account metadata
+        """Test listing account metadata for account without metadata"""
         resp, _ = self.account_client.list_account_metadata()
         self.assertHeaders(resp, 'Account', 'HEAD')
         self.assertNotIn('x-account-meta-', str(resp))
 
     @decorators.idempotent_id('e2a08b5f-3115-4768-a3ee-d4287acd6c08')
     def test_update_account_metadata_with_create_metadata(self):
-        # add metadata to account
+        """Test adding metadata to account"""
         metadata = {'test-account-meta1': 'Meta1'}
         resp, _ = self.account_client.create_update_or_delete_account_metadata(
             create_update_metadata=metadata)
@@ -305,7 +326,7 @@
 
     @decorators.idempotent_id('9f60348d-c46f-4465-ae06-d51dbd470953')
     def test_update_account_metadata_with_delete_metadata(self):
-        # delete metadata from account
+        """Test deleting metadata from account"""
         metadata = {'test-account-meta1': 'Meta1'}
         self.account_client.create_update_or_delete_account_metadata(
             create_update_metadata=metadata)
@@ -318,8 +339,11 @@
 
     @decorators.idempotent_id('64fd53f3-adbd-4639-af54-436e4982dbfb')
     def test_update_account_metadata_with_create_metadata_key(self):
-        # if the value of metadata is not set, the metadata is not
-        # registered at a server
+        """Test adding metadata to account with empty value
+
+        Adding metadata with empty value to account, the metadata is not
+        registered.
+        """
         metadata = {'test-account-meta1': ''}
         resp, _ = self.account_client.create_update_or_delete_account_metadata(
             create_update_metadata=metadata)
@@ -330,8 +354,11 @@
 
     @decorators.idempotent_id('d4d884d3-4696-4b85-bc98-4f57c4dd2bf1')
     def test_update_account_metadata_with_delete_metadata_key(self):
-        # Although the value of metadata is not set, the feature of
-        # deleting metadata is valid
+        """Test deleting metadata from account with empty value
+
+        Although the value of metadata is not set, the feature of deleting
+        metadata is valid, so the metadata is removed from account.
+        """
         metadata_1 = {'test-account-meta1': 'Meta1'}
         self.account_client.create_update_or_delete_account_metadata(
             create_update_metadata=metadata_1)
@@ -345,7 +372,11 @@
 
     @decorators.idempotent_id('8e5fc073-59b9-42ee-984a-29ed11b2c749')
     def test_update_account_metadata_with_create_and_delete_metadata(self):
-        # Send a request adding and deleting metadata requests simultaneously
+        """Test adding and deleting metadata simultaneously
+
+        Send a request adding and deleting metadata requests simultaneously,
+        both adding and deleting of metadata will succeed.
+        """
         metadata_1 = {'test-account-meta1': 'Meta1'}
         self.account_client.create_update_or_delete_account_metadata(
             create_update_metadata=metadata_1)
diff --git a/tempest/api/object_storage/test_container_sync.py b/tempest/api/object_storage/test_container_sync.py
index bdcb5ae..c5334a9 100644
--- a/tempest/api/object_storage/test_container_sync.py
+++ b/tempest/api/object_storage/test_container_sync.py
@@ -33,6 +33,8 @@
 
 
 class ContainerSyncTest(base.BaseObjectTest):
+    """Test container synchronization"""
+
     credentials = [['operator', CONF.object_storage.operator_role],
                    ['operator_alt', CONF.object_storage.operator_role]]
 
@@ -129,6 +131,7 @@
         not CONF.object_storage_feature_enabled.container_sync,
         'Old-style container sync function is disabled')
     def test_container_synchronization(self):
+        """Test container synchronization"""
         def make_headers(cont, cont_client):
             # tell first container to synchronize to a second
             client_proxy_ip = \
diff --git a/tempest/api/object_storage/test_container_sync_middleware.py b/tempest/api/object_storage/test_container_sync_middleware.py
index e77b079..db6cfa4 100644
--- a/tempest/api/object_storage/test_container_sync_middleware.py
+++ b/tempest/api/object_storage/test_container_sync_middleware.py
@@ -27,6 +27,7 @@
 
 
 class ContainerSyncMiddlewareTest(test_container_sync.ContainerSyncTest):
+    """Test containers synchronization specifying realm and cluster"""
 
     @classmethod
     def resource_setup(cls):
@@ -41,6 +42,7 @@
     @decorators.idempotent_id('ea4645a1-d147-4976-82f7-e5a7a3065f80')
     @utils.requires_ext(extension='container_sync', service='object')
     def test_container_synchronization(self):
+        """Test container synchronization specifying realm and cluster"""
         def make_headers(cont, cont_client):
             # tell first container to synchronize to a second
             account_name = cont_client.base_url.split('/')[-1]
diff --git a/tempest/api/object_storage/test_crossdomain.py b/tempest/api/object_storage/test_crossdomain.py
index 1567e06..c611ed6 100644
--- a/tempest/api/object_storage/test_crossdomain.py
+++ b/tempest/api/object_storage/test_crossdomain.py
@@ -19,6 +19,7 @@
 
 
 class CrossdomainTest(base.BaseObjectTest):
+    """Test crossdomain policy"""
 
     @classmethod
     def resource_setup(cls):
@@ -37,6 +38,7 @@
     @decorators.idempotent_id('d1b8b031-b622-4010-82f9-ff78a9e915c7')
     @utils.requires_ext(extension='crossdomain', service='object')
     def test_get_crossdomain_policy(self):
+        """Test getting crossdomain policy"""
         url = self.account_client._get_base_version_url() + "crossdomain.xml"
         resp, body = self.account_client.raw_request(url, "GET")
         self.account_client._error_checker(resp, body)
diff --git a/tempest/api/object_storage/test_object_expiry.py b/tempest/api/object_storage/test_object_expiry.py
index 86f7c8c..6f6e32f 100644
--- a/tempest/api/object_storage/test_object_expiry.py
+++ b/tempest/api/object_storage/test_object_expiry.py
@@ -21,6 +21,8 @@
 
 
 class ObjectExpiryTest(base.BaseObjectTest):
+    """Test object expiry"""
+
     @classmethod
     def resource_setup(cls):
         super(ObjectExpiryTest, cls).resource_setup()
@@ -83,6 +85,7 @@
 
     @decorators.idempotent_id('fb024a42-37f3-4ba5-9684-4f40a7910b41')
     def test_get_object_after_expiry_time(self):
+        """Test object is expired after x-delete-after time"""
         # the 10s is important, because the get calls can take 3s each
         # some times
         metadata = {'X-Delete-After': '10'}
@@ -90,5 +93,6 @@
 
     @decorators.idempotent_id('e592f18d-679c-48fe-9e36-4be5f47102c5')
     def test_get_object_at_expiry_time(self):
+        """Test object is expired at x-delete-at time"""
         metadata = {'X-Delete-At': str(int(time.time()) + 10)}
         self._test_object_expiry(metadata)
diff --git a/tempest/api/object_storage/test_object_services.py b/tempest/api/object_storage/test_object_services.py
index acb578d..4ecbcad 100644
--- a/tempest/api/object_storage/test_object_services.py
+++ b/tempest/api/object_storage/test_object_services.py
@@ -29,6 +29,7 @@
 
 
 class ObjectTest(base.BaseObjectTest):
+    """Test storage object"""
 
     @classmethod
     def resource_setup(cls):
@@ -78,6 +79,7 @@
     @decorators.attr(type='smoke')
     @decorators.idempotent_id('5b4ce26f-3545-46c9-a2ba-5754358a4c62')
     def test_create_object(self):
+        """Test creating object and checking the object's uploaded content"""
         # create object
         object_name = data_utils.rand_name(name='TestObject')
         data = data_utils.random_bytes()
@@ -97,7 +99,7 @@
 
     @decorators.idempotent_id('5daebb1d-f0d5-4dc9-b541-69672eff00b0')
     def test_create_object_with_content_disposition(self):
-        # create object with content_disposition
+        """Test creating object with content-disposition"""
         object_name = data_utils.rand_name(name='TestObject')
         data = data_utils.random_bytes()
         metadata = {}
@@ -119,7 +121,7 @@
 
     @decorators.idempotent_id('605f8317-f945-4bee-ae91-013f1da8f0a0')
     def test_create_object_with_content_encoding(self):
-        # create object with content_encoding
+        """Test creating object with content-encoding"""
         object_name = data_utils.rand_name(name='TestObject')
 
         # put compressed string
@@ -146,7 +148,7 @@
 
     @decorators.idempotent_id('73820093-0503-40b1-a478-edf0e69c7d1f')
     def test_create_object_with_etag(self):
-        # create object with etag
+        """Test creating object with Etag"""
         object_name = data_utils.rand_name(name='TestObject')
         data = data_utils.random_bytes()
         md5 = hashlib.md5(data).hexdigest()
@@ -165,8 +167,7 @@
 
     @decorators.idempotent_id('84dafe57-9666-4f6d-84c8-0814d37923b8')
     def test_create_object_with_expect_continue(self):
-        # create object with expect_continue
-
+        """Test creating object with expect_continue"""
         object_name = data_utils.rand_name(name='TestObject')
         data = data_utils.random_bytes()
 
@@ -182,7 +183,7 @@
 
     @decorators.idempotent_id('4f84422a-e2f2-4403-b601-726a4220b54e')
     def test_create_object_with_transfer_encoding(self):
-        # create object with transfer_encoding
+        """Test creating object with transfer_encoding"""
         object_name = data_utils.rand_name(name='TestObject')
         data = data_utils.random_bytes(1024)
         headers = {'Transfer-Encoding': 'chunked'}
@@ -202,7 +203,10 @@
 
     @decorators.idempotent_id('0f3d62a6-47e3-4554-b0e5-1a5dc372d501')
     def test_create_object_with_x_fresh_metadata(self):
-        # create object with x_fresh_metadata
+        """Test creating object with x-fresh-metadata
+
+        The previous added metadata will be cleared.
+        """
         object_name_base = data_utils.rand_name(name='TestObject')
         data = data_utils.random_bytes()
         metadata_1 = {'X-Object-Meta-test-meta': 'Meta'}
@@ -228,7 +232,7 @@
 
     @decorators.idempotent_id('1c7ed3e4-2099-406b-b843-5301d4811baf')
     def test_create_object_with_x_object_meta(self):
-        # create object with object_meta
+        """Test creating object with x-object-meta"""
         object_name = data_utils.rand_name(name='TestObject')
         data = data_utils.random_bytes()
         metadata = {'X-Object-Meta-test-meta': 'Meta'}
@@ -247,7 +251,7 @@
 
     @decorators.idempotent_id('e4183917-33db-4153-85cc-4dacbb938865')
     def test_create_object_with_x_object_metakey(self):
-        # create object with the blank value of metadata
+        """Test creating object with the blank value of metadata"""
         object_name = data_utils.rand_name(name='TestObject')
         data = data_utils.random_bytes()
         metadata = {'X-Object-Meta-test-meta': ''}
@@ -266,7 +270,10 @@
 
     @decorators.idempotent_id('ce798afc-b278-45de-a5ce-2ea124b98b99')
     def test_create_object_with_x_remove_object_meta(self):
-        # create object with x_remove_object_meta
+        """Test creating object with x-remove-object-meta
+
+        The metadata will be removed from the object.
+        """
         object_name = data_utils.rand_name(name='TestObject')
         data = data_utils.random_bytes()
         metadata_add = {'X-Object-Meta-test-meta': 'Meta'}
@@ -289,7 +296,11 @@
 
     @decorators.idempotent_id('ad21e342-7916-4f9e-ab62-a1f885f2aaf9')
     def test_create_object_with_x_remove_object_metakey(self):
-        # create object with the blank value of remove metadata
+        """Test creating object with the blank value of remove metadata
+
+        Creating object with blank metadata 'X-Remove-Object-Meta-test-meta',
+        metadata 'x-object-meta-test-meta' will be removed from the object.
+        """
         object_name = data_utils.rand_name(name='TestObject')
         data = data_utils.random_bytes()
         metadata_add = {'X-Object-Meta-test-meta': 'Meta'}
@@ -312,7 +323,7 @@
 
     @decorators.idempotent_id('17738d45-03bd-4d45-9e0b-7b2f58f98687')
     def test_delete_object(self):
-        # create object
+        """Test deleting object"""
         object_name = data_utils.rand_name(name='TestObject')
         data = data_utils.random_bytes()
         resp, _ = self.object_client.create_object(self.container_name,
@@ -325,7 +336,7 @@
     @decorators.attr(type='smoke')
     @decorators.idempotent_id('7a94c25d-66e6-434c-9c38-97d4e2c29945')
     def test_update_object_metadata(self):
-        # update object metadata
+        """Test updating object metadata"""
         object_name, _ = self.create_object(self.container_name)
 
         metadata = {'X-Object-Meta-test-meta': 'Meta'}
@@ -343,7 +354,7 @@
 
     @decorators.idempotent_id('48650ed0-c189-4e1e-ad6b-1d4770c6e134')
     def test_update_object_metadata_with_remove_metadata(self):
-        # update object metadata with remove metadata
+        """Test updating object metadata with remove metadata"""
         object_name = data_utils.rand_name(name='TestObject')
         data = data_utils.random_bytes()
         create_metadata = {'X-Object-Meta-test-meta1': 'Meta1'}
@@ -366,6 +377,11 @@
 
     @decorators.idempotent_id('f726174b-2ded-4708-bff7-729d12ce1f84')
     def test_update_object_metadata_with_create_and_remove_metadata(self):
+        """Test updating object with creation and deletion of metadata
+
+        Update object with creation and deletion of metadata with one
+        request, both operations will succeed.
+        """
         # creation and deletion of metadata with one request
         object_name = data_utils.rand_name(name='TestObject')
         data = data_utils.random_bytes()
@@ -392,8 +408,7 @@
 
     @decorators.idempotent_id('08854588-6449-4bb7-8cca-f2e1040f5e6f')
     def test_update_object_metadata_with_x_object_manifest(self):
-        # update object metadata with x_object_manifest
-
+        """Test updating object metadata with x_object_manifest"""
         # uploading segments
         object_name, _ = self._upload_segments()
         # creating a manifest file
@@ -418,7 +433,7 @@
 
     @decorators.idempotent_id('0dbbe89c-6811-4d84-a2df-eca2bdd40c0e')
     def test_update_object_metadata_with_x_object_metakey(self):
-        # update object metadata with a blank value of metadata
+        """Test updating object metadata with a blank value of metadata"""
         object_name, _ = self.create_object(self.container_name)
 
         update_metadata = {'X-Object-Meta-test-meta': ''}
@@ -436,7 +451,7 @@
 
     @decorators.idempotent_id('9a88dca4-b684-425b-806f-306cd0e57e42')
     def test_update_object_metadata_with_x_remove_object_metakey(self):
-        # update object metadata with a blank value of remove metadata
+        """Test updating object metadata with blank remove metadata value"""
         object_name = data_utils.rand_name(name='TestObject')
         data = data_utils.arbitrary_string()
         create_metadata = {'X-Object-Meta-test-meta': 'Meta'}
@@ -460,7 +475,7 @@
     @decorators.attr(type='smoke')
     @decorators.idempotent_id('9a447cf6-de06-48de-8226-a8c6ed31caf2')
     def test_list_object_metadata(self):
-        # get object metadata
+        """Test listing object metadata"""
         object_name = data_utils.rand_name(name='TestObject')
         data = data_utils.random_bytes()
         metadata = {'X-Object-Meta-test-meta': 'Meta'}
@@ -478,7 +493,7 @@
 
     @decorators.idempotent_id('170fb90e-f5c3-4b1f-ae1b-a18810821172')
     def test_list_no_object_metadata(self):
-        # get empty list of object metadata
+        """Test listing object metadata for object without metadata"""
         object_name, _ = self.create_object(self.container_name)
 
         resp, _ = self.object_client.list_object_metadata(
@@ -489,8 +504,7 @@
 
     @decorators.idempotent_id('23a3674c-d6de-46c3-86af-ff92bfc8a3da')
     def test_list_object_metadata_with_x_object_manifest(self):
-        # get object metadata with x_object_manifest
-
+        """Test getting object metadata with x_object_manifest"""
         # uploading segments
         object_name, _ = self._upload_segments()
         # creating a manifest file
@@ -530,7 +544,7 @@
     @decorators.attr(type='smoke')
     @decorators.idempotent_id('02610ba7-86b7-4272-9ed8-aa8d417cb3cd')
     def test_get_object(self):
-        # retrieve object's data (in response body)
+        """Test retrieving object's data (in response body)"""
 
         # create object
         object_name, data = self.create_object(self.container_name)
@@ -543,7 +557,7 @@
 
     @decorators.idempotent_id('005f9bf6-e06d-41ec-968e-96c78e0b1d82')
     def test_get_object_with_metadata(self):
-        # get object with metadata
+        """Test getting object with metadata"""
         object_name = data_utils.rand_name(name='TestObject')
         data = data_utils.random_bytes()
         metadata = {'X-Object-Meta-test-meta': 'Meta'}
@@ -562,7 +576,7 @@
 
     @decorators.idempotent_id('05a1890e-7db9-4a6c-90a8-ce998a2bddfa')
     def test_get_object_with_range(self):
-        # get object with range
+        """Test getting object with range"""
         object_name = data_utils.rand_name(name='TestObject')
         data = data_utils.random_bytes(100)
         self.object_client.create_object(self.container_name,
@@ -580,7 +594,7 @@
 
     @decorators.idempotent_id('11b4515b-7ba7-4ca8-8838-357ded86fc10')
     def test_get_object_with_x_object_manifest(self):
-        # get object with x_object_manifest
+        """Test getting object with x_object_manifest"""
 
         # uploading segments
         object_name, data_segments = self._upload_segments()
@@ -623,7 +637,7 @@
 
     @decorators.idempotent_id('c05b4013-e4de-47af-be84-e598062b16fc')
     def test_get_object_with_if_match(self):
-        # get object with if_match
+        """Test getting object with if_match"""
         object_name = data_utils.rand_name(name='TestObject')
         data = data_utils.random_bytes(10)
         create_md5 = hashlib.md5(data).hexdigest()
@@ -643,7 +657,7 @@
 
     @decorators.idempotent_id('be133639-e5d2-4313-9b1f-2d59fc054a16')
     def test_get_object_with_if_modified_since(self):
-        # get object with if_modified_since
+        """Test getting object with if_modified_since"""
         object_name = data_utils.rand_name(name='TestObject')
         data = data_utils.random_bytes()
         time_now = time.time()
@@ -663,7 +677,7 @@
 
     @decorators.idempotent_id('641500d5-1612-4042-a04d-01fc4528bc30')
     def test_get_object_with_if_none_match(self):
-        # get object with if_none_match
+        """Test getting object with if_none_match"""
         object_name = data_utils.rand_name(name='TestObject')
         data = data_utils.random_bytes()
         create_md5 = hashlib.md5(data).hexdigest()
@@ -685,7 +699,7 @@
 
     @decorators.idempotent_id('0aa1201c-10aa-467a-bee7-63cbdd463152')
     def test_get_object_with_if_unmodified_since(self):
-        # get object with if_unmodified_since
+        """Test getting object with if_unmodified_since"""
         object_name, data = self.create_object(self.container_name)
 
         time_now = time.time()
@@ -700,7 +714,7 @@
 
     @decorators.idempotent_id('94587078-475f-48f9-a40f-389c246e31cd')
     def test_get_object_with_x_newest(self):
-        # get object with x_newest
+        """Test getting object with x_newest"""
         object_name, data = self.create_object(self.container_name)
 
         list_metadata = {'X-Newest': 'true'}
@@ -713,6 +727,7 @@
 
     @decorators.idempotent_id('1a9ab572-1b66-4981-8c21-416e2a5e6011')
     def test_copy_object_in_same_container(self):
+        """Test copying object to another object in same container"""
         # create source object
         src_object_name = data_utils.rand_name(name='SrcObject')
         src_data = data_utils.random_bytes(size=len(src_object_name) * 2)
@@ -742,7 +757,7 @@
 
     @decorators.idempotent_id('2248abba-415d-410b-9c30-22dff9cd6e67')
     def test_copy_object_to_itself(self):
-        # change the content type of an existing object
+        """Test changing the content type of an existing object"""
 
         # create object
         object_name, _ = self.create_object(self.container_name)
@@ -769,6 +784,7 @@
 
     @decorators.idempotent_id('06f90388-2d0e-40aa-934c-e9a8833e958a')
     def test_copy_object_2d_way(self):
+        """Test copying object's data to the new object using COPY"""
         # create source object
         src_object_name = data_utils.rand_name(name='SrcObject')
         src_data = data_utils.random_bytes(size=len(src_object_name) * 2)
@@ -793,6 +809,7 @@
 
     @decorators.idempotent_id('aa467252-44f3-472a-b5ae-5b57c3c9c147')
     def test_copy_object_across_containers(self):
+        """Test copying object to another container"""
         # create a container to use as a source container
         src_container_name = data_utils.rand_name(name='TestSourceContainer')
         self.container_client.update_container(src_container_name)
@@ -837,6 +854,7 @@
 
     @decorators.idempotent_id('5a9e2cc6-85b6-46fc-916d-0cbb7a88e5fd')
     def test_copy_object_with_x_fresh_metadata(self):
+        """Test copying objectwith x_fresh_metadata"""
         # create source object
         metadata = {'x-object-meta-src': 'src_value'}
         src_object_name, data = self.create_object(self.container_name,
@@ -858,6 +876,7 @@
 
     @decorators.idempotent_id('a28a8b99-e701-4d7e-9d84-3b66f121460b')
     def test_copy_object_with_x_object_metakey(self):
+        """Test copying object with x_object_metakey"""
         # create source object
         metadata = {'x-object-meta-src': 'src_value'}
         src_obj_name, data = self.create_object(self.container_name,
@@ -881,6 +900,7 @@
 
     @decorators.idempotent_id('edabedca-24c3-4322-9b70-d6d9f942a074')
     def test_copy_object_with_x_object_meta(self):
+        """Test copying object with x_object_meta"""
         # create source object
         metadata = {'x-object-meta-src': 'src_value'}
         src_obj_name, data = self.create_object(self.container_name,
@@ -904,6 +924,7 @@
 
     @decorators.idempotent_id('e3e6a64a-9f50-4955-b987-6ce6767c97fb')
     def test_object_upload_in_segments(self):
+        """Test uploading object in segments"""
         # create object
         object_name = data_utils.rand_name(name='LObject')
         data = data_utils.arbitrary_string()
@@ -947,10 +968,13 @@
 
     @decorators.idempotent_id('50d01f12-526f-4360-9ac2-75dd508d7b68')
     def test_get_object_if_different(self):
-        # http://en.wikipedia.org/wiki/HTTP_ETag
-        # Make a conditional request for an object using the If-None-Match
-        # header, it should get downloaded only if the local file is different,
-        # otherwise the response code should be 304 Not Modified
+        """Test getting object content only when the local file is different
+
+        http://en.wikipedia.org/wiki/HTTP_ETag
+        Make a conditional request for an object using the If-None-Match
+        header, it should get downloaded only if the local file is different,
+        otherwise the response code should be 304 Not Modified
+        """
         object_name, data = self.create_object(self.container_name)
         # local copy is identical, no download
         md5 = hashlib.md5(data).hexdigest()
@@ -975,6 +999,7 @@
 
 
 class PublicObjectTest(base.BaseObjectTest):
+    """Test public storage object"""
 
     credentials = [['operator', CONF.object_storage.operator_role],
                    ['operator_alt', CONF.object_storage.operator_role]]
@@ -1000,9 +1025,11 @@
 
     @decorators.idempotent_id('07c9cf95-c0d4-4b49-b9c8-0ef2c9b27193')
     def test_access_public_container_object_without_using_creds(self):
-        # make container public-readable and access an object in it object
-        # anonymously, without using credentials
+        """Test accessing public container object without using credentials
 
+        Make container public-readable and access an object in it object
+        anonymously, without using credentials.
+        """
         # update container metadata to make it publicly readable
         cont_headers = {'X-Container-Read': '.r:*,.rlistings'}
         resp_meta, body = (
@@ -1040,8 +1067,11 @@
 
     @decorators.idempotent_id('54e2a2fe-42dc-491b-8270-8e4217dd4cdc')
     def test_access_public_object_with_another_user_creds(self):
-        # make container public-readable and access an object in it using
-        # another user's credentials
+        """Test accessing public object with another user's credentials
+
+        Make container public-readable and access an object in it using
+        another user's credentials.
+        """
         cont_headers = {'X-Container-Read': '.r:*,.rlistings'}
         resp_meta, body = (
             self.container_client.create_update_or_delete_container_metadata(
diff --git a/tempest/api/object_storage/test_object_slo.py b/tempest/api/object_storage/test_object_slo.py
index 8bb2e6e..7e553ca 100644
--- a/tempest/api/object_storage/test_object_slo.py
+++ b/tempest/api/object_storage/test_object_slo.py
@@ -27,6 +27,7 @@
 
 
 class ObjectSloTest(base.BaseObjectTest):
+    """Test static large object"""
 
     def setUp(self):
         super(ObjectSloTest, self).setUp()
@@ -108,7 +109,7 @@
     @decorators.idempotent_id('2c3f24a6-36e8-4711-9aa2-800ee1fc7b5b')
     @utils.requires_ext(extension='slo', service='object')
     def test_upload_manifest(self):
-        # create static large object from multipart manifest
+        """Test creating static large object from multipart manifest"""
         manifest = self._create_manifest()
 
         params = {'multipart-manifest': 'put'}
@@ -123,7 +124,10 @@
     @decorators.idempotent_id('e69ad766-e1aa-44a2-bdd2-bf62c09c1456')
     @utils.requires_ext(extension='slo', service='object')
     def test_list_large_object_metadata(self):
-        # list static large object metadata using multipart manifest
+        """Test listing static large object metadata
+
+        List static large object metadata using multipart manifest
+        """
         object_name = self._create_large_object()
 
         resp, _ = self.object_client.list_object_metadata(
@@ -135,7 +139,7 @@
     @decorators.idempotent_id('49bc49bc-dd1b-4c0f-904e-d9f10b830ee8')
     @utils.requires_ext(extension='slo', service='object')
     def test_retrieve_large_object(self):
-        # list static large object using multipart manifest
+        """Test listing static large object using multipart manifest"""
         object_name = self._create_large_object()
 
         resp, body = self.object_client.get_object(
@@ -150,7 +154,7 @@
     @decorators.idempotent_id('87b6dfa1-abe9-404d-8bf0-6c3751e6aa77')
     @utils.requires_ext(extension='slo', service='object')
     def test_delete_large_object(self):
-        # delete static large object using multipart manifest
+        """Test deleting static large object using multipart manifest"""
         object_name = self._create_large_object()
 
         params_del = {'multipart-manifest': 'delete'}
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index fc25914..cc8778b 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -187,6 +187,28 @@
     raise lib_exc.TimeoutException(message)
 
 
+def wait_for_image_imported_to_stores(client, image_id, stores):
+    """Waits for an image to be imported to all requested stores.
+
+    The client should also have build_interval and build_timeout attributes.
+    """
+
+    start = int(time.time())
+    while int(time.time()) - start < client.build_timeout:
+        image = client.show_image(image_id)
+        if image['status'] == 'active' and image['stores'] == stores:
+            return
+
+        time.sleep(client.build_interval)
+
+    message = ('Image %(image_id)s failed to import '
+               'on stores: %s' % str(image['os_glance_failed_import']))
+    caller = test_utils.find_test_caller()
+    if caller:
+        message = '(%s) %s' % (caller, message)
+    raise lib_exc.TimeoutException(message)
+
+
 def wait_for_volume_resource_status(client, resource_id, status):
     """Waits for a volume resource to reach a given status.
 
diff --git a/tempest/config.py b/tempest/config.py
index 5b7efa5..2f2c2e9 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -1199,7 +1199,7 @@
 
 The best use case is investigating used resources of one test.
 A test can be run as follows:
- $ ostestr --pdb TEST_ID
+ $ stestr run --pdb TEST_ID
 or
  $ python -m testtools.run TEST_ID"""),
 ]
diff --git a/tempest/lib/cmd/check_uuid.py b/tempest/lib/cmd/check_uuid.py
index 71ecb32..b34066f 100755
--- a/tempest/lib/cmd/check_uuid.py
+++ b/tempest/lib/cmd/check_uuid.py
@@ -16,6 +16,7 @@
 
 import argparse
 import ast
+import contextlib
 import importlib
 import inspect
 import os
@@ -28,7 +29,7 @@
 
 DECORATOR_MODULE = 'decorators'
 DECORATOR_NAME = 'idempotent_id'
-DECORATOR_IMPORT = 'tempest.%s' % DECORATOR_MODULE
+DECORATOR_IMPORT = 'tempest.lib.%s' % DECORATOR_MODULE
 IMPORT_LINE = 'from tempest.lib import %s' % DECORATOR_MODULE
 DECORATOR_TEMPLATE = "@%s.%s('%%s')" % (DECORATOR_MODULE,
                                         DECORATOR_NAME)
@@ -180,34 +181,125 @@
         elif isinstance(node, ast.ImportFrom):
             return '%s.%s' % (node.module, node.names[0].name)
 
+    @contextlib.contextmanager
+    def ignore_site_packages_paths(self):
+        """Removes site-packages directories from the sys.path
+
+        Source:
+            - StackOverflow: https://stackoverflow.com/questions/22195382/
+            - Author: https://stackoverflow.com/users/485844/
+        """
+
+        paths = sys.path
+        # remove all third-party paths
+        # so that only stdlib imports will succeed
+        sys.path = list(filter(
+            None,
+            filter(lambda i: 'site-packages' not in i, sys.path)
+        ))
+        yield
+        sys.path = paths
+
+    def is_std_lib(self, module):
+        """Checks whether the module is part of the stdlib or not
+
+        Source:
+            - StackOverflow: https://stackoverflow.com/questions/22195382/
+            - Author: https://stackoverflow.com/users/485844/
+        """
+
+        if module in sys.builtin_module_names:
+            return True
+
+        with self.ignore_site_packages_paths():
+            imported_module = sys.modules.pop(module, None)
+            try:
+                importlib.import_module(module)
+            except ImportError:
+                return False
+            else:
+                return True
+            finally:
+                if imported_module:
+                    sys.modules[module] = imported_module
+
     def _add_import_for_test_uuid(self, patcher, src_parsed, source_path):
-        with open(source_path) as f:
-            src_lines = f.read().split('\n')
-        line_no = 0
-        tempest_imports = [node for node in src_parsed.body
+        import_list = [node for node in src_parsed.body
+                       if isinstance(node, ast.Import) or
+                       isinstance(node, ast.ImportFrom)]
+
+        if not import_list:
+            print("(WARNING) %s: The file is not valid as it does not contain "
+                  "any import line! Therefore the import needed by "
+                  "@decorators.idempotent_id is not added!" % source_path)
+            return
+
+        tempest_imports = [node for node in import_list
                            if self._import_name(node) and
                            'tempest.' in self._import_name(node)]
-        if not tempest_imports:
-            import_snippet = '\n'.join(('', IMPORT_LINE, ''))
-        else:
-            for node in tempest_imports:
-                if self._import_name(node) < DECORATOR_IMPORT:
-                    continue
-                else:
-                    line_no = node.lineno
-                    import_snippet = IMPORT_LINE
-                    break
+
+        for node in tempest_imports:
+            if self._import_name(node) < DECORATOR_IMPORT:
+                continue
             else:
-                line_no = tempest_imports[-1].lineno
-                while True:
-                    if (not src_lines[line_no - 1] or
-                            getattr(self._next_node(src_parsed.body,
-                                                    tempest_imports[-1]),
-                                    'lineno') == line_no or
-                            line_no == len(src_lines)):
-                        break
-                    line_no += 1
-                import_snippet = '\n'.join((IMPORT_LINE, ''))
+                line_no = node.lineno
+                break
+        else:
+            if tempest_imports:
+                line_no = tempest_imports[-1].lineno + 1
+
+        # Insert import line between existing tempest imports
+        if tempest_imports:
+            patcher.add_patch(source_path, IMPORT_LINE, line_no)
+            return
+
+        # Group space separated imports together
+        grouped_imports = {}
+        first_import_line = import_list[0].lineno
+        for idx, import_line in enumerate(import_list, first_import_line):
+            group_no = import_line.lineno - idx
+            group = grouped_imports.get(group_no, [])
+            group.append(import_line)
+            grouped_imports[group_no] = group
+
+        if len(grouped_imports) > 3:
+            print("(WARNING) %s: The file contains more than three import "
+                  "groups! This is not valid according to the PEP8 "
+                  "style guide. " % source_path)
+
+        # Divide grouped_imports into groupes based on PEP8 style guide
+        pep8_groups = {}
+        package_name = self.package.__name__.split(".")[0]
+        for key in grouped_imports:
+            module = self._import_name(grouped_imports[key][0]).split(".")[0]
+            if module.startswith(package_name):
+                group = pep8_groups.get('3rd_group', [])
+                pep8_groups['3rd_group'] = group + grouped_imports[key]
+            elif self.is_std_lib(module):
+                group = pep8_groups.get('1st_group', [])
+                pep8_groups['1st_group'] = group + grouped_imports[key]
+            else:
+                group = pep8_groups.get('2nd_group', [])
+                pep8_groups['2nd_group'] = group + grouped_imports[key]
+
+        for node in pep8_groups.get('2nd_group', []):
+            if self._import_name(node) < DECORATOR_IMPORT:
+                continue
+            else:
+                line_no = node.lineno
+                import_snippet = IMPORT_LINE
+                break
+        else:
+            if pep8_groups.get('2nd_group', []):
+                line_no = pep8_groups['2nd_group'][-1].lineno + 1
+                import_snippet = IMPORT_LINE
+            elif pep8_groups.get('1st_group', []):
+                line_no = pep8_groups['1st_group'][-1].lineno + 1
+                import_snippet = '\n' + IMPORT_LINE
+            else:
+                line_no = pep8_groups['3rd_group'][0].lineno
+                import_snippet = IMPORT_LINE + '\n\n'
+
         patcher.add_patch(source_path, import_snippet, line_no)
 
     def get_tests(self):
diff --git a/tempest/lib/services/identity/v3/users_client.py b/tempest/lib/services/identity/v3/users_client.py
index f47730f..bba02a4 100644
--- a/tempest/lib/services/identity/v3/users_client.py
+++ b/tempest/lib/services/identity/v3/users_client.py
@@ -118,3 +118,30 @@
         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):
+        post_body = json.dumps(kwargs)
+        resp, body = self.post('/users/%s/credentials/OS-EC2' % user_id,
+                               post_body)
+        self.expected_success(201, 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/lib/cmd/test_check_uuid.py b/tempest/tests/lib/cmd/test_check_uuid.py
index 28ebca1..428e047 100644
--- a/tempest/tests/lib/cmd/test_check_uuid.py
+++ b/tempest/tests/lib/cmd/test_check_uuid.py
@@ -10,6 +10,7 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+import ast
 import importlib
 import os
 import sys
@@ -95,6 +96,8 @@
 
 
 class TestTestChecker(base.TestCase):
+    IMPORT_LINE = "from tempest.lib import decorators\n"
+
     def _test_add_uuid_to_test(self, source_file):
         class Fake_test_node():
             lineno = 1
@@ -127,55 +130,69 @@
                        "        pass")
         self._test_add_uuid_to_test(source_file)
 
+    @staticmethod
+    def get_mocked_ast_object(lineno, col_offset, module, name, object_type):
+        ast_object = mock.Mock(spec=object_type)
+        name_obj = mock.Mock()
+        ast_object.lineno = lineno
+        ast_object.col_offset = col_offset
+        name_obj.name = name
+        ast_object.module = module
+        ast_object.names = [name_obj]
+
+        return ast_object
+
     def test_add_import_for_test_uuid_no_tempest(self):
         patcher = check_uuid.SourcePatcher()
         checker = check_uuid.TestChecker(importlib.import_module('tempest'))
-        fake_file = tempfile.NamedTemporaryFile("w+t")
+        fake_file = tempfile.NamedTemporaryFile("w+t", delete=False)
+        source_code = "from unittest import mock\n"
+        fake_file.write(source_code)
+        fake_file.close()
 
         class Fake_src_parsed():
-            body = ['test_node']
-        checker._import_name = mock.Mock(return_value='fake_module')
+            body = [TestTestChecker.get_mocked_ast_object(
+                1, 4, 'unittest', 'mock', ast.ImportFrom)]
 
-        checker._add_import_for_test_uuid(patcher, Fake_src_parsed(),
+        checker._add_import_for_test_uuid(patcher, Fake_src_parsed,
                                           fake_file.name)
-        (patch_id, patch), = patcher.patches.items()
-        self.assertEqual(patcher._quote('\n' + check_uuid.IMPORT_LINE + '\n'),
-                         patch)
-        self.assertEqual('{%s:s}' % patch_id,
-                         patcher.source_files[fake_file.name])
+        patcher.apply_patches()
+
+        with open(fake_file.name, "r") as f:
+            expected_result = source_code + '\n' + TestTestChecker.IMPORT_LINE
+            self.assertTrue(expected_result == f.read())
 
     def test_add_import_for_test_uuid_tempest(self):
         patcher = check_uuid.SourcePatcher()
         checker = check_uuid.TestChecker(importlib.import_module('tempest'))
         fake_file = tempfile.NamedTemporaryFile("w+t", delete=False)
-        test1 = ("    def test_test():\n"
-                 "        pass\n")
-        test2 = ("    def test_another_test():\n"
-                 "        pass\n")
-        source_code = test1 + test2
+        source_code = "from tempest import a_fake_module\n"
         fake_file.write(source_code)
         fake_file.close()
 
-        def fake_import_name(node):
-            return node.name
-        checker._import_name = fake_import_name
+        class Fake_src_parsed:
+            body = [TestTestChecker.get_mocked_ast_object(
+                1, 4, 'tempest', 'a_fake_module', ast.ImportFrom)]
 
-        class Fake_node():
-            def __init__(self, lineno, col_offset, name):
-                self.lineno = lineno
-                self.col_offset = col_offset
-                self.name = name
-
-        class Fake_src_parsed():
-            body = [Fake_node(1, 4, 'tempest.a_fake_module'),
-                    Fake_node(3, 4, 'another_fake_module')]
-
-        checker._add_import_for_test_uuid(patcher, Fake_src_parsed(),
+        checker._add_import_for_test_uuid(patcher, Fake_src_parsed,
                                           fake_file.name)
-        (patch_id, patch), = patcher.patches.items()
-        self.assertEqual(patcher._quote(check_uuid.IMPORT_LINE + '\n'),
-                         patch)
-        expected_source = patcher._quote(test1) + '{' + patch_id + ':s}' +\
-            patcher._quote(test2)
-        self.assertEqual(expected_source,
-                         patcher.source_files[fake_file.name])
+        patcher.apply_patches()
+
+        with open(fake_file.name, "r") as f:
+            expected_result = source_code + TestTestChecker.IMPORT_LINE
+            self.assertTrue(expected_result == f.read())
+
+    def test_add_import_no_import(self):
+        patcher = check_uuid.SourcePatcher()
+        patcher.add_patch = mock.Mock()
+        checker = check_uuid.TestChecker(importlib.import_module('tempest'))
+        fake_file = tempfile.NamedTemporaryFile("w+t", delete=False)
+        fake_file.close()
+
+        class Fake_src_parsed:
+            body = []
+
+        checker._add_import_for_test_uuid(patcher, Fake_src_parsed,
+                                          fake_file.name)
+
+        self.assertTrue(not patcher.add_patch.called)
diff --git a/tempest/tests/lib/services/identity/v3/test_users_client.py b/tempest/tests/lib/services/identity/v3/test_users_client.py
index c0dfdae..7be0480 100644
--- a/tempest/tests/lib/services/identity/v3/test_users_client.py
+++ b/tempest/tests/lib/services/identity/v3/test_users_client.py
@@ -141,6 +141,35 @@
         ]
     }
 
+    FAKE_USER_EC2_CREDENTIAL_INFO = {
+        "credential": {
+            'user_id': '9beb0e12f3e5416db8d7cccfc785db3b',
+            'access': '79abf59acc77492a86170cbe2f1feafa',
+            'secret': 'c4e7d3a691fd4563873d381a40320f46',
+            'trust_id': None,
+            'tenant_id': '596557269d7b4dd78631a602eb9f151d'
+        }
+    }
+
+    FAKE_LIST_USER_EC2_CREDENTIALS = {
+        "credentials": [
+            {
+                'user_id': '9beb0e12f3e5416db8d7cccfc785db3b',
+                'access': '79abf59acc77492a86170cbe2f1feafa',
+                'secret': 'c4e7d3a691fd4563873d381a40320f46',
+                'trust_id': None,
+                'tenant_id': '596557269d7b4dd78631a602eb9f151d'
+            },
+            {
+                'user_id': '3beb0e12f3e5416db8d7cccfc785de4r',
+                'access': '45abf59acc77492a86170cbe2f1fesde',
+                'secret': 'g4e7d3a691fd4563873d381a40320e45',
+                'trust_id': None,
+                'tenant_id': '123557269d7b4dd78631a602eb9f112f'
+            }
+        ]
+    }
+
     def setUp(self):
         super(TestUsersClient, self).setUp()
         fake_auth = fake_auth_provider.FakeAuthProvider()
@@ -201,6 +230,33 @@
             user_id='817fb3c23fd7465ba6d7fe1b1320121d',
         )
 
+    def _test_create_user_ec2_credential(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.create_user_ec2_credential,
+            'tempest.lib.common.rest_client.RestClient.post',
+            self.FAKE_USER_EC2_CREDENTIAL_INFO,
+            bytes_body,
+            status=201,
+            user_id="1",
+            tenant_id="123")
+
+    def _test_show_user_ec2_credential(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.show_user_ec2_credential,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_USER_EC2_CREDENTIAL_INFO,
+            bytes_body,
+            user_id="1",
+            access="123")
+
+    def _test_list_user_ec2_credentials(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_user_ec2_credentials,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_LIST_USER_EC2_CREDENTIALS,
+            bytes_body,
+            user_id="1")
+
     def test_create_user_with_string_body(self):
         self._test_create_user()
 
@@ -255,3 +311,30 @@
             user_id='817fb3c23fd7465ba6d7fe1b1320121d',
             password='NewTempestPassword',
             original_password='OldTempestPassword')
+
+    def test_create_user_ec2_credential_with_str_body(self):
+        self._test_create_user_ec2_credential()
+
+    def test_create_user_ec2_credential_with_bytes_body(self):
+        self._test_create_user_ec2_credential(bytes_body=True)
+
+    def test_show_user_ec2_credential_with_str_body(self):
+        self._test_show_user_ec2_credential()
+
+    def test_show_user_ec2_credential_with_bytes_body(self):
+        self._test_show_user_ec2_credential(bytes_body=True)
+
+    def test_list_user_ec2_credentials_with_str_body(self):
+        self._test_list_user_ec2_credentials()
+
+    def test_list_user_ec2_credentials_with_bytes_body(self):
+        self._test_list_user_ec2_credentials(bytes_body=True)
+
+    def test_delete_user_ec2_credential(self):
+        self.check_service_client_function(
+            self.client.delete_user_ec2_credential,
+            'tempest.lib.common.rest_client.RestClient.delete',
+            {},
+            user_id="123",
+            access="1234",
+            status=204)