Merge "Improve patch and patchobject Base test case method attributes."
diff --git a/README.rst b/README.rst
index f8059ae..044ae09 100644
--- a/README.rst
+++ b/README.rst
@@ -15,7 +15,7 @@
 
 This is a set of integration tests to be run against a live OpenStack
 cluster. Tempest has batteries of tests for OpenStack API validation,
-Scenarios, and other specific tests useful in validating an OpenStack
+scenarios, and other specific tests useful in validating an OpenStack
 deployment.
 
 Design Principles
diff --git a/REVIEWING.rst b/REVIEWING.rst
index 766d0c6..a880181 100644
--- a/REVIEWING.rst
+++ b/REVIEWING.rst
@@ -42,8 +42,8 @@
 
 API Stability
 -------------
-Tests should only be added for a published stable APIs. If a patch contains
-tests for an API which hasn't been marked as stable or for an API that which
+Tests should only be added for published stable APIs. If a patch contains
+tests for an API which hasn't been marked as stable or for an API which
 doesn't conform to the `API stability guidelines
 <https://wiki.openstack.org/wiki/Governance/Approved/APIStability>`_ then it
 should not be approved.
diff --git a/releasenotes/notes/identity-v3-project-tags-client-36683c6a8644e54b.yaml b/releasenotes/notes/identity-v3-project-tags-client-36683c6a8644e54b.yaml
new file mode 100644
index 0000000..dfbcc7d
--- /dev/null
+++ b/releasenotes/notes/identity-v3-project-tags-client-36683c6a8644e54b.yaml
@@ -0,0 +1,12 @@
+---
+features:
+  - |
+    Add ``project_tags_client`` to the identity v3 library. This feature
+    enables the possibility of invoking the following API actions:
+
+    * update_project_tag
+    * list_project_tags
+    * update_all_project_tags
+    * check_project_tag_existence
+    * delete_project_tag
+    * delete_all_project_tags
diff --git a/tempest/api/compute/servers/test_device_tagging.py b/tempest/api/compute/servers/test_device_tagging.py
index 43046ca..b0d527c 100644
--- a/tempest/api/compute/servers/test_device_tagging.py
+++ b/tempest/api/compute/servers/test_device_tagging.py
@@ -47,8 +47,8 @@
             raise cls.skipException('Neutron is required')
         if not CONF.validation.run_validation:
             raise cls.skipException('Validation must be enabled')
-        if (not CONF.compute_feature_enabled.config_drive
-            and not CONF.compute_feature_enabled.metadata_service):
+        if (not CONF.compute_feature_enabled.config_drive and
+                not CONF.compute_feature_enabled.metadata_service):
             raise cls.skipException('One of metadata or config drive must be '
                                     'enabled')
 
diff --git a/tempest/api/compute/servers/test_server_actions.py b/tempest/api/compute/servers/test_server_actions.py
index 5c3e9f0..bbec30c 100644
--- a/tempest/api/compute/servers/test_server_actions.py
+++ b/tempest/api/compute/servers/test_server_actions.py
@@ -586,7 +586,7 @@
             server_info = self.client.show_server(self.server_id)['server']
             if 'SHELVED' in server_info['status']:
                 self.client.unshelve_server(self.server_id)
-        self.addOnException(_unshelve_server)
+        self.addCleanup(_unshelve_server)
 
         server = self.client.show_server(self.server_id)['server']
         image_name = server['name'] + '-shelved'
diff --git a/tempest/api/compute/servers/test_servers_negative.py b/tempest/api/compute/servers/test_servers_negative.py
index 9b545af..e944c28 100644
--- a/tempest/api/compute/servers/test_servers_negative.py
+++ b/tempest/api/compute/servers/test_servers_negative.py
@@ -481,7 +481,7 @@
             server_info = self.client.show_server(self.server_id)['server']
             if 'SHELVED' in server_info['status']:
                 self.client.unshelve_server(self.server_id)
-        self.addOnException(_unshelve_server)
+        self.addCleanup(_unshelve_server)
 
         server = self.client.show_server(self.server_id)['server']
         image_name = server['name'] + '-shelved'
diff --git a/tempest/api/identity/admin/v3/test_project_tags.py b/tempest/api/identity/admin/v3/test_project_tags.py
new file mode 100644
index 0000000..d05173b
--- /dev/null
+++ b/tempest/api/identity/admin/v3/test_project_tags.py
@@ -0,0 +1,66 @@
+# Copyright 2018 AT&T Corporation.
+# 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.
+
+import testtools
+
+from tempest.api.identity import base
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+CONF = config.CONF
+
+
+class IdentityV3ProjectTagsTest(base.BaseIdentityV3AdminTest):
+
+    @decorators.idempotent_id('7c123aac-999d-416a-a0fb-84b915ab10de')
+    @testtools.skipUnless(CONF.identity_feature_enabled.project_tags,
+                          'Project tags not available.')
+    def test_list_update_delete_project_tags(self):
+        project = self.setup_test_project()
+
+        # Create a tag for testing.
+        tag = data_utils.rand_name('tag')
+        # NOTE(felipemonteiro): The response body for create is empty.
+        self.project_tags_client.update_project_tag(project['id'], tag)
+
+        # Verify that the tag was created.
+        self.project_tags_client.check_project_tag_existence(
+            project['id'], tag)
+
+        # Verify that updating the project tags works.
+        tags_to_update = [data_utils.rand_name('tag') for _ in range(3)]
+        updated_tags = self.project_tags_client.update_all_project_tags(
+            project['id'], tags_to_update)['tags']
+        self.assertEqual(sorted(tags_to_update), sorted(updated_tags))
+
+        # Verify that listing project tags works.
+        retrieved_tags = self.project_tags_client.list_project_tags(
+            project['id'])['tags']
+        self.assertEqual(sorted(tags_to_update), sorted(retrieved_tags))
+
+        # Verify that deleting a project tag works.
+        self.project_tags_client.delete_project_tag(
+            project['id'], tags_to_update[0])
+        self.assertRaises(lib_exc.NotFound,
+                          self.project_tags_client.check_project_tag_existence,
+                          project['id'], tags_to_update[0])
+
+        # Verify that deleting all project tags works.
+        self.project_tags_client.delete_all_project_tags(project['id'])
+        retrieved_tags = self.project_tags_client.list_project_tags(
+            project['id'])['tags']
+        self.assertEmpty(retrieved_tags)
diff --git a/tempest/api/identity/base.py b/tempest/api/identity/base.py
index 9edccbb..6edb8f3 100644
--- a/tempest/api/identity/base.py
+++ b/tempest/api/identity/base.py
@@ -228,6 +228,7 @@
         cls.domain_config_client = cls.os_admin.domain_config_client
         cls.endpoint_filter_client = cls.os_admin.endpoint_filter_client
         cls.endpoint_groups_client = cls.os_admin.endpoint_groups_client
+        cls.project_tags_client = cls.os_admin.project_tags_client
 
         if CONF.identity.admin_domain_scope:
             # NOTE(andreaf) When keystone policy requires it, the identity
diff --git a/tempest/api/image/v1/test_images.py b/tempest/api/image/v1/test_images.py
index 76723f4..2432c8b 100644
--- a/tempest/api/image/v1/test_images.py
+++ b/tempest/api/image/v1/test_images.py
@@ -132,8 +132,8 @@
     @classmethod
     def skip_checks(cls):
         super(ListImagesTest, cls).skip_checks()
-        if (len(CONF.image.container_formats) < 2
-           or len(CONF.image.disk_formats) < 2):
+        if (len(CONF.image.container_formats) < 2 or
+                len(CONF.image.disk_formats) < 2):
             skip_msg = ("%s skipped as multiple container formats "
                         "or disk formats are not available." % cls.__name__)
             raise cls.skipException(skip_msg)
@@ -227,8 +227,8 @@
             self.assertEqual(image['disk_format'], self.disk_format_alt)
         result_set = set(map(lambda x: x['id'], images_list))
         self.assertTrue(self.same_disk_format_set <= result_set)
-        self.assertFalse(self.created_set - self.same_disk_format_set
-                         <= result_set)
+        self.assertFalse(self.created_set - self.same_disk_format_set <=
+                         result_set)
 
     @decorators.idempotent_id('2143655d-96d9-4bec-9188-8674206b4b3b')
     def test_index_container_format(self):
@@ -238,8 +238,8 @@
             self.assertEqual(image['container_format'], self.container_format)
         result_set = set(map(lambda x: x['id'], images_list))
         self.assertTrue(self.same_container_format_set <= result_set)
-        self.assertFalse(self.created_set - self.same_container_format_set
-                         <= result_set)
+        self.assertFalse(self.created_set - self.same_container_format_set <=
+                         result_set)
 
     @decorators.idempotent_id('feb32ac6-22bb-4a16-afd8-9454bb714b14')
     def test_index_max_size(self):
diff --git a/tempest/api/volume/admin/test_volume_hosts.py b/tempest/api/volume/admin/test_volume_hosts.py
index ce0cbd2..7e53ce8 100644
--- a/tempest/api/volume/admin/test_volume_hosts.py
+++ b/tempest/api/volume/admin/test_volume_hosts.py
@@ -46,8 +46,8 @@
         # show host API should fail (return code: 404). The cinder-volume host
         # is presented in format: <host-name>@driver-name.
         c_vol_hosts = [host['host_name'] for host in hosts
-                       if (host['service'] == 'cinder-volume'
-                           and host['service-state'] == 'enabled')]
+                       if (host['service'] == 'cinder-volume' and
+                           host['service-state'] == 'enabled')]
         self.assertNotEmpty(c_vol_hosts,
                             "No available cinder-volume host is found, "
                             "all hosts that found are: %s" % hosts)
diff --git a/tempest/api/volume/test_volumes_snapshots_list.py b/tempest/api/volume/test_volumes_snapshots_list.py
index 507df1f..f12bfd8 100644
--- a/tempest/api/volume/test_volumes_snapshots_list.py
+++ b/tempest/api/volume/test_volumes_snapshots_list.py
@@ -28,13 +28,11 @@
     @classmethod
     def resource_setup(cls):
         super(VolumesSnapshotListTestJSON, cls).resource_setup()
-        cls.snapshot_id_list = []
         volume_origin = cls.create_volume()
 
         # Create snapshots with params
         for _ in range(3):
             snapshot = cls.create_snapshot(volume_origin['id'])
-            cls.snapshot_id_list.append(snapshot['id'])
         cls.snapshot = snapshot
 
     def _list_by_param_values_and_assert(self, with_detail=False, **params):
@@ -151,10 +149,14 @@
     @decorators.idempotent_id('05489dde-44bc-4961-a1f5-3ce7ee7824f7')
     def test_snapshot_list_param_marker(self):
         # The list of snapshots should end before the provided marker
-        params = {'marker': self.snapshot_id_list[1]}
+        snap_list = self.snapshots_client.list_snapshots()['snapshots']
+        # list_snapshots will take the reverse order as they are created.
+        snapshot_id_list = [snap['id'] for snap in snap_list][::-1]
+
+        params = {'marker': snapshot_id_list[1]}
         snap_list = self.snapshots_client.list_snapshots(**params)['snapshots']
         fetched_list_id = [snap['id'] for snap in snap_list]
         # Verify the list of snapshots ends before the provided
         # marker(second snapshot), therefore only the first snapshot
         # should displayed.
-        self.assertEqual(self.snapshot_id_list[:1], fetched_list_id)
+        self.assertEqual(snapshot_id_list[:1], fetched_list_id)
diff --git a/tempest/clients.py b/tempest/clients.py
index b06eafb..d75a712 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -197,6 +197,8 @@
         self.endpoint_groups_client = self.identity_v3.EndPointGroupsClient(
             **params_v3)
         self.catalog_client = self.identity_v3.CatalogClient(**params_v3)
+        self.project_tags_client = self.identity_v3.ProjectTagsClient(
+            **params_v3)
 
         # Token clients do not use the catalog. They only need default_params.
         # They read auth_url, so they should only be set if the corresponding
diff --git a/tempest/cmd/cleanup_service.py b/tempest/cmd/cleanup_service.py
index 025959a..27e1bc1 100644
--- a/tempest/cmd/cleanup_service.py
+++ b/tempest/cmd/cleanup_service.py
@@ -104,11 +104,11 @@
             self.tenant_filter['tenant_id'] = self.tenant_id
 
     def _filter_by_tenant_id(self, item_list):
-        if (item_list is None
-                or not item_list
-                or not hasattr(self, 'tenant_id')
-                or self.tenant_id is None
-                or 'tenant_id' not in item_list[0]):
+        if (item_list is None or
+                not item_list or
+                not hasattr(self, 'tenant_id') or
+                self.tenant_id is None or
+                'tenant_id' not in item_list[0]):
             return item_list
 
         return [item for item in item_list
@@ -816,8 +816,8 @@
             if not self.is_save_state:
                 roles = [role for role in roles if
                          (role['id'] not in
-                          self.saved_state_json['roles'].keys()
-                          and role['name'] != CONF.identity.admin_role)]
+                          self.saved_state_json['roles'].keys() and
+                          role['name'] != CONF.identity.admin_role)]
                 LOG.debug("List count, %s Roles after reconcile", len(roles))
             return roles
         except Exception:
@@ -852,13 +852,16 @@
     def list(self):
         projects = self.client.list_projects()['projects']
         if not self.is_save_state:
-            projects = [project for project in projects if (project['id']
-                        not in self.saved_state_json['projects'].keys()
-                        and project['name'] != CONF.auth.admin_project_name)]
+            project_ids = self.saved_state_json['projects']
+            projects = [project
+                        for project in projects
+                        if (project['id'] not in project_ids and
+                            project['name'] != CONF.auth.admin_project_name)]
 
         if self.is_preserve:
-            projects = [project for project in projects if project['name']
-                        not in CONF_PROJECTS]
+            projects = [project
+                        for project in projects
+                        if project['name'] not in CONF_PROJECTS]
 
         LOG.debug("List count, %s Projects after reconcile", len(projects))
         return projects
diff --git a/tempest/hacking/checks.py b/tempest/hacking/checks.py
index aae685c..b6e7f8c 100644
--- a/tempest/hacking/checks.py
+++ b/tempest/hacking/checks.py
@@ -228,12 +228,12 @@
     if 'tempest/lib/' not in filename:
         return
 
-    if not ('from tempest' in logical_line
-            or 'import tempest' in logical_line):
+    if not ('from tempest' in logical_line or
+            'import tempest' in logical_line):
         return
 
-    if ('from tempest.lib' in logical_line
-        or 'import tempest.lib' in logical_line):
+    if ('from tempest.lib' in logical_line or
+            'import tempest.lib' in logical_line):
         return
 
     msg = ("T112: tempest.lib should not import local tempest code to avoid "
@@ -266,9 +266,9 @@
     if 'tempest/lib/' not in filename:
         return
 
-    if ('tempest.config' in logical_line
-        or 'from tempest import config' in logical_line
-        or 'oslo_config' in logical_line):
+    if ('tempest.config' in logical_line or
+            'from tempest import config' in logical_line or
+            'oslo_config' in logical_line):
         msg = ('T114: tempest.lib can not have any dependency on tempest '
                'config.')
         yield(0, msg)
diff --git a/tempest/lib/base.py b/tempest/lib/base.py
index 33a32ee..3be55c0 100644
--- a/tempest/lib/base.py
+++ b/tempest/lib/base.py
@@ -43,8 +43,7 @@
         super(BaseTestCase, self).setUp()
         if not self.setUpClassCalled:
             raise RuntimeError("setUpClass does not calls the super's "
-                               "setUpClass in the "
-                               + self.__class__.__name__)
+                               "setUpClass in {!r}".format(type(self)))
         test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
         try:
             test_timeout = int(test_timeout)
@@ -62,7 +61,7 @@
             stderr = self.useFixture(fixtures.StringStream('stderr')).stream
             self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
         if (os.environ.get('OS_LOG_CAPTURE') != 'False' and
-            os.environ.get('OS_LOG_CAPTURE') != '0'):
+                os.environ.get('OS_LOG_CAPTURE') != '0'):
             self.useFixture(fixtures.LoggerFixture(nuke_handlers=False,
                                                    format=self.log_format,
                                                    level=None))
diff --git a/tempest/lib/cli/output_parser.py b/tempest/lib/cli/output_parser.py
index 2edd5c1..a7d5e49 100644
--- a/tempest/lib/cli/output_parser.py
+++ b/tempest/lib/cli/output_parser.py
@@ -37,8 +37,8 @@
     items = []
     tables_ = tables(output_lines)
     for table_ in tables_:
-        if ('Property' not in table_['headers']
-            or 'Value' not in table_['headers']):
+        if ('Property' not in table_['headers'] or
+                'Value' not in table_['headers']):
             raise exceptions.InvalidStructure()
         item = {}
         for value in table_['values']:
diff --git a/tempest/lib/cmd/check_uuid.py b/tempest/lib/cmd/check_uuid.py
index 101d692..d1f0888 100755
--- a/tempest/lib/cmd/check_uuid.py
+++ b/tempest/lib/cmd/check_uuid.py
@@ -103,7 +103,7 @@
     def _modules_search(self):
         """Recursive search for python modules in base package"""
         modules = []
-        for root, dirs, files in os.walk(self.base_path):
+        for root, _, files in os.walk(self.base_path):
             if not os.path.exists(os.path.join(root, '__init__.py')):
                 continue
             root_package = self._path_to_package(root)
@@ -121,10 +121,10 @@
         idempotent_id = None
         for decorator in test_node.decorator_list:
             if (hasattr(decorator, 'func') and
-                hasattr(decorator.func, 'attr') and
-                decorator.func.attr == DECORATOR_NAME and
-                hasattr(decorator.func, 'value') and
-                decorator.func.value.id == DECORATOR_MODULE):
+                    hasattr(decorator.func, 'attr') and
+                    decorator.func.attr == DECORATOR_NAME and
+                    hasattr(decorator.func, 'value') and
+                    decorator.func.value.id == DECORATOR_MODULE):
                 for arg in decorator.args:
                     idempotent_id = ast.literal_eval(arg)
         return idempotent_id
@@ -165,8 +165,8 @@
 
     @staticmethod
     def _is_test_method(node):
-        return (node.__class__ is ast.FunctionDef
-                and node.name.startswith('test_'))
+        return (node.__class__ is ast.FunctionDef and
+                node.name.startswith('test_'))
 
     @staticmethod
     def _next_node(body, node):
diff --git a/tempest/lib/common/dynamic_creds.py b/tempest/lib/common/dynamic_creds.py
index 4f1a883..f27e926 100644
--- a/tempest/lib/common/dynamic_creds.py
+++ b/tempest/lib/common/dynamic_creds.py
@@ -338,15 +338,15 @@
                 credentials = self._create_creds(roles=credential_type)
             self._creds[str(credential_type)] = credentials
             # Maintained until tests are ported
-            LOG.info("Acquired dynamic creds:\n credentials: %s", credentials)
-            if (self.neutron_available and
-                self.create_networks):
+            LOG.info("Acquired dynamic creds:\n"
+                     " credentials: %s", credentials)
+            if (self.neutron_available and self.create_networks):
                 network, subnet, router = self._create_network_resources(
                     credentials.tenant_id)
                 credentials.set_resources(network=network, subnet=subnet,
                                           router=router)
-                LOG.info("Created isolated network resources for : \n"
-                         + " credentials: %s", credentials)
+                LOG.info("Created isolated network resources for:\n"
+                         " credentials: %s", credentials)
         return credentials
 
     def get_primary_creds(self):
diff --git a/tempest/lib/services/identity/v3/__init__.py b/tempest/lib/services/identity/v3/__init__.py
index a539d08..f302455 100644
--- a/tempest/lib/services/identity/v3/__init__.py
+++ b/tempest/lib/services/identity/v3/__init__.py
@@ -33,6 +33,8 @@
 from tempest.lib.services.identity.v3.oauth_token_client import \
     OAUTHTokenClient
 from tempest.lib.services.identity.v3.policies_client import PoliciesClient
+from tempest.lib.services.identity.v3.project_tags_client import \
+    ProjectTagsClient
 from tempest.lib.services.identity.v3.projects_client import ProjectsClient
 from tempest.lib.services.identity.v3.regions_client import RegionsClient
 from tempest.lib.services.identity.v3.role_assignments_client import \
@@ -49,6 +51,6 @@
            'EndPointsClient', 'EndPointsFilterClient', 'GroupsClient',
            'IdentityClient', 'InheritedRolesClient', 'OAUTHConsumerClient',
            'OAUTHTokenClient', 'PoliciesClient', 'ProjectsClient',
-           'RegionsClient', 'RoleAssignmentsClient', 'RolesClient',
-           'ServicesClient', 'V3TokenClient', 'TrustsClient', 'UsersClient',
-           'VersionsClient']
+           'ProjectTagsClient', 'RegionsClient', 'RoleAssignmentsClient',
+           'RolesClient', 'ServicesClient', 'V3TokenClient', 'TrustsClient',
+           'UsersClient', 'VersionsClient']
diff --git a/tempest/lib/services/identity/v3/project_tags_client.py b/tempest/lib/services/identity/v3/project_tags_client.py
new file mode 100644
index 0000000..dd1a2a5
--- /dev/null
+++ b/tempest/lib/services/identity/v3/project_tags_client.py
@@ -0,0 +1,80 @@
+# Copyright 2018 AT&T Corporation.
+# 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 oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class ProjectTagsClient(rest_client.RestClient):
+    api_version = "v3"
+
+    def update_project_tag(self, project_id, tag):
+        """Updates the specified tag and adds it to the project's list of tags.
+
+        """
+        url = 'projects/%s/tags/%s' % (project_id, tag)
+        resp, body = self.put(url, '{}')
+        # NOTE(felipemonteiro): This API endpoint returns 201 AND an empty
+        # response body, which is consistent with the spec:
+        # https://specs.openstack.org/openstack/api-wg/guidelines/tags.html#addressing-individual-tags
+        self.expected_success(201, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def list_project_tags(self, project_id):
+        """List tags for a project."""
+        url = "projects/%s/tags" % project_id
+        resp, body = self.get(url)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def update_all_project_tags(self, project_id, tags, **kwargs):
+        """Updates all the tags for a project.
+
+        Any existing tags not specified will be deleted.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/identity/v3/#modify-tag-list-for-a-project
+        """
+        body = {'tags': tags}
+        if kwargs:
+            body.update(kwargs)
+        put_body = json.dumps(body)
+        resp, body = self.put('projects/%s/tags' % project_id, put_body)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def check_project_tag_existence(self, project_id, tag):
+        """Check if a project contains a tag."""
+        url = 'projects/%s/tags/%s' % (project_id, tag)
+        resp, body = self.get(url)
+        self.expected_success(204, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def delete_project_tag(self, project_id, tag):
+        """Delete a project tag."""
+        url = 'projects/%s/tags/%s' % (project_id, tag)
+        resp, body = self.delete(url)
+        self.expected_success(204, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def delete_all_project_tags(self, project_id):
+        """Delete all tags from a project."""
+        resp, body = self.delete('projects/%s/tags' % project_id)
+        self.expected_success(204, resp.status)
+        return rest_client.ResponseBody(resp, body)
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index c78646f..cf53b67 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -662,8 +662,8 @@
                 addresses = (server['addresses'][network['name']]
                              if network else [])
             for address in addresses:
-                if (address['version'] == CONF.validation.ip_version_for_ssh
-                        and address['OS-EXT-IPS:type'] == 'fixed'):
+                if (address['version'] == CONF.validation.ip_version_for_ssh and  # noqa
+                        address['OS-EXT-IPS:type'] == 'fixed'):
                     return address['addr']
             raise exceptions.ServerUnreachable(server_id=server['id'])
         else:
@@ -792,8 +792,8 @@
         port_map = [(p["id"], fxip["ip_address"])
                     for p in ports
                     for fxip in p["fixed_ips"]
-                    if netutils.is_valid_ipv4(fxip["ip_address"])
-                    and p['status'] in p_status]
+                    if (netutils.is_valid_ipv4(fxip["ip_address"]) and
+                        p['status'] in p_status)]
         inactive = [p for p in ports if p['status'] != 'ACTIVE']
         if inactive:
             LOG.warning("Instance has ports that are not ACTIVE: %s", inactive)
diff --git a/tempest/scenario/test_network_advanced_server_ops.py b/tempest/scenario/test_network_advanced_server_ops.py
index e4ab11c..87ce951 100644
--- a/tempest/scenario/test_network_advanced_server_ops.py
+++ b/tempest/scenario/test_network_advanced_server_ops.py
@@ -43,8 +43,8 @@
     @classmethod
     def skip_checks(cls):
         super(TestNetworkAdvancedServerOps, cls).skip_checks()
-        if not (CONF.network.project_networks_reachable
-                or CONF.network.public_network_id):
+        if not (CONF.network.project_networks_reachable or
+                CONF.network.public_network_id):
             msg = ('Either project_networks_reachable must be "true", or '
                    'public_network_id must be defined.')
             raise cls.skipException(msg)
diff --git a/tempest/scenario/test_network_basic_ops.py b/tempest/scenario/test_network_basic_ops.py
index fd9c985..bcd4ddb 100644
--- a/tempest/scenario/test_network_basic_ops.py
+++ b/tempest/scenario/test_network_basic_ops.py
@@ -81,8 +81,8 @@
     @classmethod
     def skip_checks(cls):
         super(TestNetworkBasicOps, cls).skip_checks()
-        if not (CONF.network.project_networks_reachable
-                or CONF.network.public_network_id):
+        if not (CONF.network.project_networks_reachable or
+                CONF.network.public_network_id):
             msg = ('Either project_networks_reachable must be "true", or '
                    'public_network_id must be defined.')
             raise cls.skipException(msg)
diff --git a/tempest/scenario/test_network_v6.py b/tempest/scenario/test_network_v6.py
index 9f4e62b..e4e39c3 100644
--- a/tempest/scenario/test_network_v6.py
+++ b/tempest/scenario/test_network_v6.py
@@ -38,11 +38,11 @@
     @classmethod
     def skip_checks(cls):
         super(TestGettingAddress, cls).skip_checks()
-        if not (CONF.network_feature_enabled.ipv6
-                and CONF.network_feature_enabled.ipv6_subnet_attributes):
+        if not (CONF.network_feature_enabled.ipv6 and
+                CONF.network_feature_enabled.ipv6_subnet_attributes):
             raise cls.skipException('IPv6 or its attributes not supported')
-        if not (CONF.network.project_networks_reachable
-                or CONF.network.public_network_id):
+        if not (CONF.network.project_networks_reachable or
+                CONF.network.public_network_id):
             msg = ('Either project_networks_reachable must be "true", or '
                    'public_network_id must be defined.')
             raise cls.skipException(msg)
diff --git a/tempest/test.py b/tempest/test.py
index 27e0165..f2babbb 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -68,9 +68,9 @@
 def validate_tearDownClass():
     if at_exit_set:
         LOG.error(
-            "tearDownClass does not call the super's "
-            "tearDownClass in these classes: \n"
-            + str(at_exit_set))
+            "tearDownClass does not call the super's tearDownClass in "
+            "these classes:\n"
+            "  %s", at_exit_set)
 
 
 atexit.register(validate_tearDownClass)
@@ -582,8 +582,8 @@
         super(BaseTestCase, self).setUp()
         if not self.__setupclass_called:
             raise RuntimeError("setUpClass does not calls the super's"
-                               "setUpClass in the "
-                               + self.__class__.__name__)
+                               "setUpClass in the " +
+                               self.__class__.__name__)
         at_exit_set.add(self.__class__)
         test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
         try:
@@ -602,7 +602,7 @@
             stderr = self.useFixture(fixtures.StringStream('stderr')).stream
             self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
         if (os.environ.get('OS_LOG_CAPTURE') != 'False' and
-            os.environ.get('OS_LOG_CAPTURE') != '0'):
+                os.environ.get('OS_LOG_CAPTURE') != '0'):
             self.useFixture(fixtures.LoggerFixture(nuke_handlers=False,
                                                    format=self.log_format,
                                                    level=None))
diff --git a/tempest/tests/lib/services/identity/v3/test_project_tags_client.py b/tempest/tests/lib/services/identity/v3/test_project_tags_client.py
new file mode 100644
index 0000000..2d65a29
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_project_tags_client.py
@@ -0,0 +1,104 @@
+# Copyright 2018 AT&T Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from tempest.lib.services.identity.v3 import project_tags_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestProjectTagsClient(base.BaseServiceTest):
+
+    FAKE_PROJECT_ID = "0c4e939acacf4376bdcd1129f1a054ad"
+
+    FAKE_PROJECT_TAG = "foo"
+
+    FAKE_PROJECT_TAGS = ["foo", "bar"]
+
+    def setUp(self):
+        super(TestProjectTagsClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = project_tags_client.ProjectTagsClient(fake_auth,
+                                                            'identity',
+                                                            'regionOne')
+
+    def _test_update_project_tag(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.update_project_tag,
+            'tempest.lib.common.rest_client.RestClient.put',
+            {},
+            bytes_body,
+            project_id=self.FAKE_PROJECT_ID,
+            tag=self.FAKE_PROJECT_TAG,
+            status=201)
+
+    def _test_list_project_tags(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_project_tags,
+            'tempest.lib.common.rest_client.RestClient.get',
+            {"tags": self.FAKE_PROJECT_TAGS},
+            bytes_body,
+            project_id=self.FAKE_PROJECT_ID)
+
+    def _test_update_all_project_tags(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.update_all_project_tags,
+            'tempest.lib.common.rest_client.RestClient.put',
+            {"tags": self.FAKE_PROJECT_TAGS},
+            bytes_body,
+            project_id=self.FAKE_PROJECT_ID,
+            tags=self.FAKE_PROJECT_TAGS)
+
+    def test_update_project_tag_with_str_body(self):
+        self._test_update_project_tag()
+
+    def test_update_project_tag_with_bytes_body(self):
+        self._test_update_project_tag(bytes_body=True)
+
+    def test_list_project_tags_with_str_body(self):
+        self._test_list_project_tags()
+
+    def test_list_project_tags_with_bytes_body(self):
+        self._test_list_project_tags(bytes_body=True)
+
+    def test_update_all_project_tags_with_str_body(self):
+        self._test_update_all_project_tags()
+
+    def test_update_all_project_tags_with_bytes_body(self):
+        self._test_update_all_project_tags(bytes_body=True)
+
+    def test_check_project_project_tag_existence(self):
+        self.check_service_client_function(
+            self.client.check_project_tag_existence,
+            'tempest.lib.common.rest_client.RestClient.get',
+            {},
+            project_id=self.FAKE_PROJECT_ID,
+            tag=self.FAKE_PROJECT_TAG,
+            status=204)
+
+    def test_delete_project_tag(self):
+        self.check_service_client_function(
+            self.client.delete_project_tag,
+            'tempest.lib.common.rest_client.RestClient.delete',
+            {},
+            project_id=self.FAKE_PROJECT_ID,
+            tag=self.FAKE_PROJECT_TAG,
+            status=204)
+
+    def test_delete_all_project_tags(self):
+        self.check_service_client_function(
+            self.client.delete_all_project_tags,
+            'tempest.lib.common.rest_client.RestClient.delete',
+            {},
+            project_id=self.FAKE_PROJECT_ID,
+            status=204)
diff --git a/tools/generate-tempest-plugins-list.py b/tools/generate-tempest-plugins-list.py
index dd05438..bbb9019 100644
--- a/tools/generate-tempest-plugins-list.py
+++ b/tools/generate-tempest-plugins-list.py
@@ -75,11 +75,13 @@
 # json library won't choke.
 projects = sorted(filter(is_in_openstack_namespace, json.loads(r.read()[4:])))
 
-# Retrieve projects having no deb, ui or spec namespace as those namespaces
-# do not contains tempest plugins.
-projects_list = [i for i in projects if not (i.startswith('openstack/deb-') or
-                                             i.endswith('-ui') or
-                                             i.endswith('-specs'))]
+# Retrieve projects having no deb, puppet, ui or spec namespace as those
+# namespaces do not contains tempest plugins.
+projects_list = [i for i in projects if not (
+    i.startswith('openstack/deb-') or
+    i.startswith('openstack/puppet-') or
+    i.endswith('-ui') or
+    i.endswith('-specs'))]
 
 found_plugins = list(filter(has_tempest_plugin, projects_list))