Merge "Fix tempest cleanup AttributeError"
diff --git a/.zuul.yaml b/.zuul.yaml
index ef9b0eb..8576455 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -222,6 +222,13 @@
           LIVE_MIGRATION_AVAILABLE: true
           USE_BLOCK_MIGRATION_FOR_LIVE_MIGRATION: true
 
+- job:
+    name: tempest-multinode-full-py3
+    parent: tempest-multinode-full
+    vars:
+      devstack_localrc:
+        USE_PYTHON3: true
+
 - nodeset:
     name: openstack-bionic-node
     nodes:
@@ -272,6 +279,20 @@
       tempest_concurrency: 2
 
 - job:
+    name: tempest-slow-py3
+    parent: tempest-slow
+    vars:
+      devstack_localrc:
+        USE_PYTHON3: true
+      devstack_services:
+        s-account: false
+        s-container: false
+        s-object: false
+        s-proxy: false
+        # without Swift, c-bak cannot run (in the Gate at least)
+        c-bak: false
+
+- job:
     name: tempest-full-rocky
     parent: tempest-full
     nodeset: openstack-single-node-xenial
@@ -500,6 +521,8 @@
             irrelevant-files: *tempest-irrelevant-files
         - tempest-multinode-full:
             irrelevant-files: *tempest-irrelevant-files
+        - tempest-multinode-full-py3:
+            irrelevant-files: *tempest-irrelevant-files
         - tempest-tox-plugin-sanity-check:
             irrelevant-files:
               - ^(test-|)requirements.txt$
@@ -513,6 +536,8 @@
               # tools/ is not here since this relies on a script in tools/.
         - tempest-slow:
             irrelevant-files: *tempest-irrelevant-files
+        - tempest-slow-py3:
+            irrelevant-files: *tempest-irrelevant-files
         - nova-live-migration:
             voting: false
             irrelevant-files: *tempest-irrelevant-files
@@ -556,7 +581,7 @@
       jobs:
         - nova-multiattach:
             irrelevant-files: *tempest-irrelevant-files
-        - tempest-slow:
+        - tempest-slow-py3:
             irrelevant-files: *tempest-irrelevant-files
         - neutron-grenade-multinode:
             irrelevant-files: *tempest-irrelevant-files
diff --git a/releasenotes/notes/Placement-client-for-placement-based-minimum-bw-allocation-27ed0938118752b6.yaml b/releasenotes/notes/Placement-client-for-placement-based-minimum-bw-allocation-27ed0938118752b6.yaml
new file mode 100644
index 0000000..21b74a6
--- /dev/null
+++ b/releasenotes/notes/Placement-client-for-placement-based-minimum-bw-allocation-27ed0938118752b6.yaml
@@ -0,0 +1,17 @@
+---
+features:
+  - |
+    Add basic read-only Placement client to Tempest to make possible the
+    testing of the placement based bandwidth allocation feature.
+    The following API calls are available for tempest from now:
+
+    * GET /allocation_candidates
+    * GET /allocations/{consumer_uuid}
+
+    Add new config group ``placement``, with the config options:
+
+    * ``endpoint_type`` to use for communication with placement service.
+    * ``catalog_type`` of the placement service.
+    * ``region`` as the placement region name to use.
+    * ``min_microversion`` and ``max_microversion`` as the range between
+      placement API requests are sent.
diff --git a/tempest/api/compute/admin/test_keypairs_v210.py b/tempest/api/compute/admin/test_keypairs_v210.py
index 24ea8a1..40ed532 100644
--- a/tempest/api/compute/admin/test_keypairs_v210.py
+++ b/tempest/api/compute/admin/test_keypairs_v210.py
@@ -56,7 +56,7 @@
         self.assertEqual(first_keyname, keypair_detail['name'])
         self.assertEqual(user_id, keypair_detail['user_id'],
                          "The fetched keypair is not for requested user!")
-        # Create a admin keypair
+        # Create an admin keypair
         admin_keypair = self.create_keypair(keypair_type='ssh',
                                             client=self.client)
         admin_keypair.pop('private_key', None)
diff --git a/tempest/api/compute/admin/test_live_migration.py b/tempest/api/compute/admin/test_live_migration.py
index 5a60dc6..b1a7c52 100644
--- a/tempest/api/compute/admin/test_live_migration.py
+++ b/tempest/api/compute/admin/test_live_migration.py
@@ -50,8 +50,6 @@
         # a subnet so the instance being migrated has a single port, but
         # we need that to make sure we are properly updating the port
         # host bindings during the live migration.
-        # TODO(mriedem): SSH validation before and after the instance is
-        # live migrated would be a nice test wrinkle addition.
         cls.set_network_resources(network=True, subnet=True)
         super(LiveMigrationTestBase, cls).setup_credentials()
 
diff --git a/tempest/api/compute/admin/test_servers_on_multinodes.py b/tempest/api/compute/admin/test_servers_on_multinodes.py
index 5cd98f4..bebc8c5 100644
--- a/tempest/api/compute/admin/test_servers_on_multinodes.py
+++ b/tempest/api/compute/admin/test_servers_on_multinodes.py
@@ -105,7 +105,7 @@
         asserts the servers are in the group and on different hosts.
         """
         hosts = self._create_servers_with_group('anti-affinity')
-        hostnames = hosts.values()
+        hostnames = list(hosts.values())
         self.assertNotEqual(hostnames[0], hostnames[1],
                             'Servers are on the same host: %s' % hosts)
 
@@ -120,6 +120,6 @@
         asserts the servers are in the group and on same host.
         """
         hosts = self._create_servers_with_group('affinity')
-        hostnames = hosts.values()
+        hostnames = list(hosts.values())
         self.assertEqual(hostnames[0], hostnames[1],
                          'Servers are on the different hosts: %s' % hosts)
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index 09dd409..624a99e 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -457,7 +457,7 @@
             else:
                 msg = ('When validation.connect_method equals floating, '
                        'validation_resources cannot be None')
-                raise exceptions.InvalidParam(invalid_param=msg)
+                raise lib_exc.InvalidParam(invalid_param=msg)
         elif CONF.validation.connect_method == 'fixed':
             addresses = server['addresses'][CONF.validation.network_for_ssh]
             for address in addresses:
diff --git a/tempest/api/compute/servers/test_attach_interfaces.py b/tempest/api/compute/servers/test_attach_interfaces.py
index 05c2a28..bea23d9 100644
--- a/tempest/api/compute/servers/test_attach_interfaces.py
+++ b/tempest/api/compute/servers/test_attach_interfaces.py
@@ -256,8 +256,8 @@
         if not (CONF.auth.use_dynamic_credentials and
                 CONF.auth.create_isolated_networks and
                 not CONF.network.shared_physical_network):
-                raise self.skipException("Only owner network supports "
-                                         "creating interface by fixed ip.")
+            raise self.skipException("Only owner network supports "
+                                     "creating interface by fixed ip.")
 
         server, ifs = self._create_server_get_interfaces()
         interface_count = len(ifs)
diff --git a/tempest/api/identity/admin/v3/test_domain_configuration.py b/tempest/api/identity/admin/v3/test_domain_configuration.py
index c4e0622..c0b18ca 100644
--- a/tempest/api/identity/admin/v3/test_domain_configuration.py
+++ b/tempest/api/identity/admin/v3/test_domain_configuration.py
@@ -21,6 +21,10 @@
 
 
 class DomainConfigurationTestJSON(base.BaseIdentityV3AdminTest):
+    # NOTE: force_tenant_isolation is true in the base class by default but
+    # overridden to false here to allow test execution for clouds using the
+    # pre-provisioned credentials provider.
+    force_tenant_isolation = False
 
     custom_config = {
         "identity": {
diff --git a/tempest/api/identity/admin/v3/test_domains_negative.py b/tempest/api/identity/admin/v3/test_domains_negative.py
index 56f7d32..b3c68fb 100644
--- a/tempest/api/identity/admin/v3/test_domains_negative.py
+++ b/tempest/api/identity/admin/v3/test_domains_negative.py
@@ -20,6 +20,10 @@
 
 
 class DomainsNegativeTestJSON(base.BaseIdentityV3AdminTest):
+    # NOTE: force_tenant_isolation is true in the base class by default but
+    # overridden to false here to allow test execution for clouds using the
+    # pre-provisioned credentials provider.
+    force_tenant_isolation = False
 
     @decorators.attr(type=['negative', 'gate'])
     @decorators.idempotent_id('1f3fbff5-4e44-400d-9ca1-d953f05f609b')
diff --git a/tempest/api/identity/admin/v3/test_oauth_consumers.py b/tempest/api/identity/admin/v3/test_oauth_consumers.py
index 062cce5..7a85f84 100644
--- a/tempest/api/identity/admin/v3/test_oauth_consumers.py
+++ b/tempest/api/identity/admin/v3/test_oauth_consumers.py
@@ -21,6 +21,10 @@
 
 
 class OAUTHConsumersV3Test(base.BaseIdentityV3AdminTest):
+    # NOTE: force_tenant_isolation is true in the base class by default but
+    # overridden to false here to allow test execution for clouds using the
+    # pre-provisioned credentials provider.
+    force_tenant_isolation = False
 
     def _create_consumer(self):
         """Creates a consumer with a random description."""
diff --git a/tempest/api/identity/admin/v3/test_project_tags.py b/tempest/api/identity/admin/v3/test_project_tags.py
index d05173b..b7878a8 100644
--- a/tempest/api/identity/admin/v3/test_project_tags.py
+++ b/tempest/api/identity/admin/v3/test_project_tags.py
@@ -25,6 +25,10 @@
 
 
 class IdentityV3ProjectTagsTest(base.BaseIdentityV3AdminTest):
+    # NOTE: force_tenant_isolation is true in the base class by default but
+    # overridden to false here to allow test execution for clouds using the
+    # pre-provisioned credentials provider.
+    force_tenant_isolation = False
 
     @decorators.idempotent_id('7c123aac-999d-416a-a0fb-84b915ab10de')
     @testtools.skipUnless(CONF.identity_feature_enabled.project_tags,
diff --git a/tempest/api/identity/admin/v3/test_projects.py b/tempest/api/identity/admin/v3/test_projects.py
index f75edaa..e42610a 100644
--- a/tempest/api/identity/admin/v3/test_projects.py
+++ b/tempest/api/identity/admin/v3/test_projects.py
@@ -12,11 +12,15 @@
 #    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
 
+CONF = config.CONF
+
 
 class ProjectsTestJSON(base.BaseIdentityV3AdminTest):
 
@@ -176,6 +180,10 @@
         self.assertEqual(resp2_en, resp3_en)
 
     @decorators.idempotent_id('59398d4a-5dc5-4f86-9a4c-c26cc804d6c6')
+    @testtools.skipIf(CONF.identity_feature_enabled.immutable_user_source,
+                      'Skipped because environment has an '
+                      'immutable user source and solely '
+                      'provides read-only access to users.')
     def test_associate_user_to_project(self):
         # Associate a user to a project
         # Create a Project
diff --git a/tempest/api/identity/admin/v3/test_tokens.py b/tempest/api/identity/admin/v3/test_tokens.py
index 8ae43d6..5f1b58d 100644
--- a/tempest/api/identity/admin/v3/test_tokens.py
+++ b/tempest/api/identity/admin/v3/test_tokens.py
@@ -42,10 +42,10 @@
         user = self.create_test_user(password=user_password)
 
         # Create a couple projects
-        project1_name = data_utils.rand_name(name='project')
+        project1_name = data_utils.rand_name(name=self.__class__.__name__)
         project1 = self.setup_test_project(name=project1_name)
 
-        project2_name = data_utils.rand_name(name='project')
+        project2_name = data_utils.rand_name(name=self.__class__.__name__)
         project2 = self.setup_test_project(name=project2_name)
         self.addCleanup(self.projects_client.delete_project, project2['id'])
 
diff --git a/tempest/api/identity/admin/v3/test_trusts.py b/tempest/api/identity/admin/v3/test_trusts.py
index 83b3c30..54a5ab7 100644
--- a/tempest/api/identity/admin/v3/test_trusts.py
+++ b/tempest/api/identity/admin/v3/test_trusts.py
@@ -49,7 +49,8 @@
 
     def create_trustor_and_roles(self):
         # create a project that trusts will be granted on
-        trustor_project_name = data_utils.rand_name(name='project')
+        trustor_project_name = data_utils.rand_name(
+            name=self.__class__.__name__)
         project = self.projects_client.create_project(
             trustor_project_name,
             domain_id=CONF.identity.default_domain_id)['project']
diff --git a/tempest/cmd/account_generator.py b/tempest/cmd/account_generator.py
index 25e91aa..7ea0099 100755
--- a/tempest/cmd/account_generator.py
+++ b/tempest/cmd/account_generator.py
@@ -291,13 +291,16 @@
 
 
 def main(opts=None):
-    setup_logging()
+    log_warning = False
     if not opts:
-        LOG.warning("Use of: 'tempest-account-generator' is deprecated, "
-                    "please use: 'tempest account-generator'")
+        log_warning = True
         opts = get_options()
     if opts.config_file:
         config.CONF.set_config_path(opts.config_file)
+    setup_logging()
+    if log_warning:
+        LOG.warning("Use of: 'tempest-account-generator' is deprecated, "
+                    "please use: 'tempest account-generator'")
     if opts.os_tenant_name:
         LOG.warning("'os-tenant-name' and 'OS_TENANT_NAME' are both "
                     "deprecated, please use 'os-project-name' or "
diff --git a/tempest/common/compute.py b/tempest/common/compute.py
index 375113d..1489e60 100644
--- a/tempest/common/compute.py
+++ b/tempest/common/compute.py
@@ -168,10 +168,19 @@
                   'imageRef': image_id,
                   'size': CONF.volume.volume_size}
         volume = volumes_client.create_volume(**params)
-        waiters.wait_for_volume_resource_status(volumes_client,
-                                                volume['volume']['id'],
-                                                'available')
-
+        try:
+            waiters.wait_for_volume_resource_status(volumes_client,
+                                                    volume['volume']['id'],
+                                                    'available')
+        except Exception:
+            with excutils.save_and_reraise_exception():
+                try:
+                    volumes_client.delete_volume(volume['volume']['id'])
+                    volumes_client.wait_for_resource_deletion(
+                        volume['volume']['id'])
+                except Exception as exc:
+                    LOG.exception("Deleting volume %s failed, exception %s",
+                                  volume['volume']['id'], exc)
         bd_map_v2 = [{
             'uuid': volume['volume']['id'],
             'source_type': 'volume',
diff --git a/tempest/common/utils/linux/remote_client.py b/tempest/common/utils/linux/remote_client.py
index 52ccfa9..49d9742 100644
--- a/tempest/common/utils/linux/remote_client.py
+++ b/tempest/common/utils/linux/remote_client.py
@@ -98,6 +98,7 @@
     def get_nic_name_by_ip(self, address):
         cmd = "ip -o addr | awk '/%s/ {print $2}'" % address
         nic = self.exec_command(cmd)
+        LOG.debug('(get_nic_name_by_ip) Command result: %s', nic)
         return nic.strip().strip(":").split('@')[0].lower()
 
     def get_dns_servers(self):
diff --git a/tempest/config.py b/tempest/config.py
index 716c000..e431754 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -365,6 +365,38 @@
                     "with format 'X.Y' or string 'latest'"),
 ]
 
+placement_group = cfg.OptGroup(name='placement',
+                               title='Placement Service Options')
+
+PlacementGroup = [
+    cfg.StrOpt('endpoint_type',
+               default='public',
+               choices=['public', 'admin', 'internal'],
+               help="The endpoint type to use for the placement service."),
+    cfg.StrOpt('catalog_type',
+               default='placement',
+               help="Catalog type of the Placement service."),
+    cfg.StrOpt('region',
+               default='RegionOne',
+               help="The placement region name to use. If empty, the value "
+                    "of [identity]/region is used instead. If no such region "
+                    "is found in the service catalog, the first region found "
+                    "is used."),
+    cfg.StrOpt('min_microversion',
+               default=None,
+               help="Lower version of the test target microversion range. "
+                    "The format is 'X.Y', where 'X' and 'Y' are int values. "
+                    "Valid values are string with format 'X.Y' or string "
+                    "'latest'"),
+    cfg.StrOpt('max_microversion',
+               default=None,
+               help="Upper version of the test target microversion range. "
+                    "The format is 'X.Y', where 'X' and 'Y' are int values. "
+                    "Valid values are string with format 'X.Y' or string "
+                    "'latest'"),
+]
+
+
 compute_features_group = cfg.OptGroup(name='compute-feature-enabled',
                                       title="Enabled Compute Service Features")
 
@@ -1096,6 +1128,7 @@
     (scenario_group, ScenarioGroup),
     (service_available_group, ServiceAvailableGroup),
     (debug_group, DebugGroup),
+    (placement_group, PlacementGroup),
     (None, DefaultGroup)
 ]
 
diff --git a/tempest/lib/cmd/check_uuid.py b/tempest/lib/cmd/check_uuid.py
index 82fcd0b..ac40eef 100755
--- a/tempest/lib/cmd/check_uuid.py
+++ b/tempest/lib/cmd/check_uuid.py
@@ -233,8 +233,8 @@
                           if self._is_test_case(module, node))
             for node in test_cases:
                 for subnode in filter(self._is_test_method, node.body):
-                        test_name = '%s.%s' % (node.name, subnode.name)
-                        tests[module_name]['tests'][test_name] = subnode
+                    test_name = '%s.%s' % (node.name, subnode.name)
+                    tests[module_name]['tests'][test_name] = subnode
         return tests
 
     @staticmethod
diff --git a/tempest/lib/common/rest_client.py b/tempest/lib/common/rest_client.py
index ec46caf..e612bd1 100644
--- a/tempest/lib/common/rest_client.py
+++ b/tempest/lib/common/rest_client.py
@@ -374,7 +374,7 @@
         return self.request('COPY', url, extra_headers, headers)
 
     def get_versions(self):
-        """Get the versions on a endpoint from the keystone catalog
+        """Get the versions on an endpoint from the keystone catalog
 
         This method will make a GET request on the baseurl from the keystone
         catalog to return a list of API versions. It is expected that a GET
@@ -526,7 +526,7 @@
         if (resp.status == 205 and
             0 != len(set(resp.keys()) - set(('status',)) -
                      self.response_header_lc - self.general_header_lc)):
-                        raise exceptions.ResponseWithEntity()
+            raise exceptions.ResponseWithEntity()
         # NOTE(afazekas)
         # Now the swift sometimes (delete not empty container)
         # returns with non json error response, we can create new rest class
diff --git a/tempest/lib/services/clients.py b/tempest/lib/services/clients.py
index 833cfd6..90debd9 100644
--- a/tempest/lib/services/clients.py
+++ b/tempest/lib/services/clients.py
@@ -18,7 +18,6 @@
 import importlib
 import inspect
 import sys
-import warnings
 
 from debtcollector import removals
 from oslo_log import log as logging
@@ -32,9 +31,9 @@
 from tempest.lib.services import image
 from tempest.lib.services import network
 from tempest.lib.services import object_storage
+from tempest.lib.services import placement
 from tempest.lib.services import volume
 
-warnings.simplefilter("once")
 LOG = logging.getLogger(__name__)
 
 
@@ -46,6 +45,7 @@
     """
     return {
         'compute': compute,
+        'placement': placement,
         'identity.v2': identity.v2,
         'identity.v3': identity.v3,
         'image.v1': image.v1,
diff --git a/tempest/lib/services/placement/__init__.py b/tempest/lib/services/placement/__init__.py
new file mode 100644
index 0000000..5c20c57
--- /dev/null
+++ b/tempest/lib/services/placement/__init__.py
@@ -0,0 +1,18 @@
+# Copyright (c) 2019 Ericsson
+#
+#    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.placement.placement_client import \
+    PlacementClient
+
+__all__ = ['PlacementClient']
diff --git a/tempest/lib/services/placement/base_placement_client.py b/tempest/lib/services/placement/base_placement_client.py
new file mode 100644
index 0000000..505a515
--- /dev/null
+++ b/tempest/lib/services/placement/base_placement_client.py
@@ -0,0 +1,43 @@
+# Copyright (c) 2019 Ericsson
+#
+#    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.common import api_version_utils
+from tempest.lib.common import rest_client
+
+PLACEMENT_MICROVERSION = None
+
+
+class BasePlacementClient(rest_client.RestClient):
+
+    api_microversion_header_name = 'OpenStack-API-Version'
+    version_header_value = 'placement %s'
+
+    def get_headers(self):
+        headers = super(BasePlacementClient, self).get_headers()
+        if PLACEMENT_MICROVERSION:
+            headers[self.api_microversion_header_name] = \
+                self.version_header_value % PLACEMENT_MICROVERSION
+        return headers
+
+    def request(self, method, url, extra_headers=False, headers=None,
+                body=None, chunked=False):
+        resp, resp_body = super(BasePlacementClient, self).request(
+            method, url, extra_headers, headers, body, chunked)
+        if (PLACEMENT_MICROVERSION and
+            PLACEMENT_MICROVERSION != api_version_utils.LATEST_MICROVERSION):
+            api_version_utils.assert_version_header_matches_request(
+                self.api_microversion_header_name,
+                self.version_header_value % PLACEMENT_MICROVERSION,
+                resp)
+        return resp, resp_body
diff --git a/tempest/lib/services/placement/placement_client.py b/tempest/lib/services/placement/placement_client.py
new file mode 100644
index 0000000..2c6d919
--- /dev/null
+++ b/tempest/lib/services/placement/placement_client.py
@@ -0,0 +1,50 @@
+# Copyright (c) 2019 Ericsson
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
+from tempest.lib.services.placement import base_placement_client
+
+
+class PlacementClient(base_placement_client.BasePlacementClient):
+
+    def list_allocation_candidates(self, **params):
+        """List allocation candidates.
+
+        For full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/placement/#list-allocation-candidates
+        """
+        url = '/allocation_candidates'
+        if params:
+            url += '?%s' % urllib.urlencode(params)
+        resp, body = self.get(url)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def list_allocations(self, consumer_uuid):
+        """List all allocation records for the consumer.
+
+        For full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/placement/#list-allocations
+        """
+        url = '/allocations/%s' % consumer_uuid
+        resp, body = self.get(url)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
diff --git a/tempest/scenario/test_network_advanced_server_ops.py b/tempest/scenario/test_network_advanced_server_ops.py
index e94ce3d..4be2b29 100644
--- a/tempest/scenario/test_network_advanced_server_ops.py
+++ b/tempest/scenario/test_network_advanced_server_ops.py
@@ -230,6 +230,32 @@
 
         self.assertNotEqual(src_host, dst_host)
 
+    @decorators.idempotent_id('03fd1562-faad-11e7-9ea0-fa163e65f5ce')
+    @testtools.skipUnless(CONF.compute_feature_enabled.live_migration,
+                          'Live migration is not available.')
+    @testtools.skipUnless(CONF.compute.min_compute_nodes > 1,
+                          'Less than 2 compute nodes, skipping multinode '
+                          'tests.')
+    @decorators.attr(type='slow')
+    @utils.services('compute', 'network')
+    def test_server_connectivity_live_migration(self):
+        keypair = self.create_keypair()
+        server = self._setup_server(keypair)
+        floating_ip = self._setup_network(server, keypair)
+        self._wait_server_status_and_check_network_connectivity(
+            server, keypair, floating_ip)
+
+        block_migration = (CONF.compute_feature_enabled.
+                           block_migration_for_live_migration)
+        self.admin_servers_client.live_migrate_server(
+            server['id'], host=None, block_migration=block_migration,
+            disk_over_commit=False)
+        waiters.wait_for_server_status(self.servers_client,
+                                       server['id'], 'ACTIVE')
+
+        self._wait_server_status_and_check_network_connectivity(
+            server, keypair, floating_ip)
+
     @decorators.skip_because(bug='1788403')
     @decorators.idempotent_id('25b188d7-0183-4b1e-a11d-15840c8e2fd6')
     @testtools.skipUnless(CONF.compute_feature_enabled.cold_migration,
diff --git a/tempest/scenario/test_network_basic_ops.py b/tempest/scenario/test_network_basic_ops.py
index c11070c..7992585 100644
--- a/tempest/scenario/test_network_basic_ops.py
+++ b/tempest/scenario/test_network_basic_ops.py
@@ -579,10 +579,17 @@
         initial_dns_server = '1.2.3.4'
         alt_dns_server = '9.8.7.6'
 
-        # renewal should be immediate.
-        # Timeouts are suggested by salvatore-orlando in
+        # Original timeouts are suggested by salvatore-orlando in
         # https://bugs.launchpad.net/neutron/+bug/1412325/comments/3
-        renew_delay = CONF.network.build_interval
+        #
+        # Compared to that renew_delay was increased, because
+        # busybox's udhcpc accepts SIGUSR1 as a renew request. Internally
+        # it goes into RENEW_REQUESTED state. If it receives a 2nd SIGUSR1
+        # signal while in that state then it calls the deconfig script
+        # ("/sbin/cirros-dhcpc deconfig" in sufficiently new cirros versions)
+        # which leads to the address being transiently deconfigured which
+        # for our case is unwanted.
+        renew_delay = 3 * CONF.network.build_interval
         renew_timeout = CONF.network.build_timeout
 
         self._setup_network_and_servers(dns_nameservers=[initial_dns_server])
diff --git a/tempest/tests/cmd/test_run.py b/tempest/tests/cmd/test_run.py
index e159cdc..00f8bc5 100644
--- a/tempest/tests/cmd/test_run.py
+++ b/tempest/tests/cmd/test_run.py
@@ -108,6 +108,27 @@
         subprocess.call(['stestr', 'init'])
         self.assertRunExit(['tempest', 'run', '--regex', 'passing'], 0)
 
+    def test_tempest_run_failing(self):
+        self.assertRunExit(['tempest', 'run', '--regex', 'failing'], 1)
+
+    def test_tempest_run_failing_with_stestr_repository(self):
+        subprocess.call(['stestr', 'init'])
+        self.assertRunExit(['tempest', 'run', '--regex', 'failing'], 1)
+
+    def test_tempest_run_blackregex_failing(self):
+        self.assertRunExit(['tempest', 'run', '--black-regex', 'failing'], 0)
+
+    def test_tempest_run_blackregex_failing_with_stestr_repository(self):
+        subprocess.call(['stestr', 'init'])
+        self.assertRunExit(['tempest', 'run', '--black-regex', 'failing'], 0)
+
+    def test_tempest_run_blackregex_passing(self):
+        self.assertRunExit(['tempest', 'run', '--black-regex', 'passing'], 1)
+
+    def test_tempest_run_blackregex_passing_with_stestr_repository(self):
+        subprocess.call(['stestr', 'init'])
+        self.assertRunExit(['tempest', 'run', '--black-regex', 'passing'], 1)
+
     def test_tempest_run_fails(self):
         self.assertRunExit(['tempest', 'run'], 1)
 
@@ -136,7 +157,7 @@
         whitelist_file.write('passing'.encode('utf-8'))
         self.assertRunExit(['tempest', 'run', '--whitelist-file=%s' % path], 0)
 
-    def test_tempest_run_with_whitelist_with_regex(self):
+    def test_tempest_run_with_whitelist_regex_include_pass_check_fail(self):
         fd, path = tempfile.mkstemp()
         self.addCleanup(os.remove, path)
         whitelist_file = os.fdopen(fd, 'wb', 0)
@@ -145,11 +166,72 @@
         self.assertRunExit(['tempest', 'run', '--whitelist-file=%s' % path,
                             '--regex', 'fail'], 1)
 
+    def test_tempest_run_with_whitelist_regex_include_pass_check_pass(self):
+        fd, path = tempfile.mkstemp()
+        self.addCleanup(os.remove, path)
+        whitelist_file = os.fdopen(fd, 'wb', 0)
+        self.addCleanup(whitelist_file.close)
+        whitelist_file.write('passing'.encode('utf-8'))
+        self.assertRunExit(['tempest', 'run', '--whitelist-file=%s' % path,
+                            '--regex', 'passing'], 0)
+
+    def test_tempest_run_with_whitelist_regex_include_fail_check_pass(self):
+        fd, path = tempfile.mkstemp()
+        self.addCleanup(os.remove, path)
+        whitelist_file = os.fdopen(fd, 'wb', 0)
+        self.addCleanup(whitelist_file.close)
+        whitelist_file.write('failing'.encode('utf-8'))
+        self.assertRunExit(['tempest', 'run', '--whitelist-file=%s' % path,
+                            '--regex', 'pass'], 1)
+
     def test_tempest_run_passes_with_config_file(self):
         self.assertRunExit(['tempest', 'run',
                             '--config-file', self.stestr_conf_file,
                             '--regex', 'passing'], 0)
 
+    def test_tempest_run_with_blacklist_failing(self):
+        fd, path = tempfile.mkstemp()
+        self.addCleanup(os.remove, path)
+        blacklist_file = os.fdopen(fd, 'wb', 0)
+        self.addCleanup(blacklist_file.close)
+        blacklist_file.write('failing'.encode('utf-8'))
+        self.assertRunExit(['tempest', 'run', '--blacklist-file=%s' % path], 0)
+
+    def test_tempest_run_with_blacklist_passing(self):
+        fd, path = tempfile.mkstemp()
+        self.addCleanup(os.remove, path)
+        blacklist_file = os.fdopen(fd, 'wb', 0)
+        self.addCleanup(blacklist_file.close)
+        blacklist_file.write('passing'.encode('utf-8'))
+        self.assertRunExit(['tempest', 'run', '--blacklist-file=%s' % path], 1)
+
+    def test_tempest_run_with_blacklist_regex_exclude_fail_check_pass(self):
+        fd, path = tempfile.mkstemp()
+        self.addCleanup(os.remove, path)
+        blacklist_file = os.fdopen(fd, 'wb', 0)
+        self.addCleanup(blacklist_file.close)
+        blacklist_file.write('failing'.encode('utf-8'))
+        self.assertRunExit(['tempest', 'run', '--blacklist-file=%s' % path,
+                            '--regex', 'pass'], 0)
+
+    def test_tempest_run_with_blacklist_regex_exclude_pass_check_pass(self):
+        fd, path = tempfile.mkstemp()
+        self.addCleanup(os.remove, path)
+        blacklist_file = os.fdopen(fd, 'wb', 0)
+        self.addCleanup(blacklist_file.close)
+        blacklist_file.write('passing'.encode('utf-8'))
+        self.assertRunExit(['tempest', 'run', '--blacklist-file=%s' % path,
+                            '--regex', 'pass'], 1)
+
+    def test_tempest_run_with_blacklist_regex_exclude_pass_check_fail(self):
+        fd, path = tempfile.mkstemp()
+        self.addCleanup(os.remove, path)
+        blacklist_file = os.fdopen(fd, 'wb', 0)
+        self.addCleanup(blacklist_file.close)
+        blacklist_file.write('passing'.encode('utf-8'))
+        self.assertRunExit(['tempest', 'run', '--blacklist-file=%s' % path,
+                            '--regex', 'fail'], 1)
+
 
 class TestConfigPathCheck(base.TestCase):
     def setUp(self):
diff --git a/tempest/tests/cmd/test_workspace.py b/tempest/tests/cmd/test_workspace.py
index 65481de..b4f6c5f 100644
--- a/tempest/tests/cmd/test_workspace.py
+++ b/tempest/tests/cmd/test_workspace.py
@@ -133,12 +133,89 @@
                          "None or empty name is specified."
                          " Please specify correct name for workspace.\n")
 
+    def test_workspace_manager_rename_with_existing_name(self):
+        new_name = self.name
+        with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+            ex = self.assertRaises(SystemExit,
+                                   self.workspace_manager.rename_workspace,
+                                   self.name, new_name)
+        self.assertEqual(1, ex.code)
+        self.assertEqual(mock_stdout.getvalue(),
+                         "A workspace already exists with name: %s.\n"
+                         % new_name)
+
+    def test_workspace_manager_rename_no_exist_old_name(self):
+        old_name = ""
+        new_name = data_utils.rand_uuid()
+        with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+            ex = self.assertRaises(SystemExit,
+                                   self.workspace_manager.rename_workspace,
+                                   old_name, new_name)
+        self.assertEqual(1, ex.code)
+        self.assertEqual(mock_stdout.getvalue(),
+                         "A workspace was not found with name: %s\n"
+                         % old_name)
+
+    def test_workspace_manager_rename_integer_data(self):
+        old_name = self.name
+        new_name = 12345
+        self.workspace_manager.rename_workspace(old_name, new_name)
+        self.assertIsNone(self.workspace_manager.get_workspace(old_name))
+        self.assertIsNotNone(self.workspace_manager.get_workspace(new_name))
+
+    def test_workspace_manager_rename_alphanumeric_data(self):
+        old_name = self.name
+        new_name = 'abc123'
+        self.workspace_manager.rename_workspace(old_name, new_name)
+        self.assertIsNone(self.workspace_manager.get_workspace(old_name))
+        self.assertIsNotNone(self.workspace_manager.get_workspace(new_name))
+
     def test_workspace_manager_move(self):
         new_path = tempfile.mkdtemp()
         self.addCleanup(shutil.rmtree, new_path, ignore_errors=True)
         self.workspace_manager.move_workspace(self.name, new_path)
         self.assertEqual(
             self.workspace_manager.get_workspace(self.name), new_path)
+        # NOTE(mbindlish): Also checking for the workspace that it
+        # shouldn't exist in old path
+        self.assertNotEqual(
+            self.workspace_manager.get_workspace(self.name), self.path)
+
+    def test_workspace_manager_move_wrong_path(self):
+        new_path = 'wrong/path'
+        with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+            ex = self.assertRaises(SystemExit,
+                                   self.workspace_manager.move_workspace,
+                                   self.name, new_path)
+        self.assertEqual(1, ex.code)
+        self.assertEqual(mock_stdout.getvalue(),
+                         "Path does not exist.\n")
+
+    def test_workspace_manager_move_wrong_workspace(self):
+        workspace_name = "wrong_workspace_name"
+        new_path = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, new_path, ignore_errors=True)
+        with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+            ex = self.assertRaises(SystemExit,
+                                   self.workspace_manager.move_workspace,
+                                   workspace_name, new_path)
+        self.assertEqual(1, ex.code)
+        self.assertEqual(mock_stdout.getvalue(),
+                         "A workspace was not found with name: %s\n"
+                         % workspace_name)
+
+    def test_workspace_manager_move_no_workspace_name(self):
+        workspace_name = ""
+        new_path = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, new_path, ignore_errors=True)
+        with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+            ex = self.assertRaises(SystemExit,
+                                   self.workspace_manager.move_workspace,
+                                   workspace_name, new_path)
+        self.assertEqual(1, ex.code)
+        self.assertEqual(mock_stdout.getvalue(),
+                         "A workspace was not found with name: %s\n"
+                         % workspace_name)
 
     def test_workspace_manager_move_no_workspace_path(self):
         new_path = ""
@@ -155,6 +232,30 @@
         self.workspace_manager.remove_workspace_entry(self.name)
         self.assertIsNone(self.workspace_manager.get_workspace(self.name))
 
+    def test_workspace_manager_remove_entry_no_name(self):
+        no_name = ""
+        with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+            ex = self.assertRaises(SystemExit,
+                                   self.workspace_manager.
+                                   remove_workspace_entry,
+                                   no_name)
+        self.assertEqual(1, ex.code)
+        self.assertEqual(mock_stdout.getvalue(),
+                         "A workspace was not found with name: %s\n"
+                         % no_name)
+
+    def test_workspace_manager_remove_entry_wrong_name(self):
+        wrong_name = "wrong_name"
+        with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+            ex = self.assertRaises(SystemExit,
+                                   self.workspace_manager.
+                                   remove_workspace_entry,
+                                   wrong_name)
+        self.assertEqual(1, ex.code)
+        self.assertEqual(mock_stdout.getvalue(),
+                         "A workspace was not found with name: %s\n"
+                         % wrong_name)
+
     def test_workspace_manager_remove_directory(self):
         path = self.workspace_manager.remove_workspace_entry(self.name)
         self.workspace_manager.remove_workspace_directory(path)
@@ -188,8 +289,11 @@
                                    nonexistent_name)
         self.assertEqual(1, ex.code)
         self.assertEqual(mock_stdout.getvalue(),
-                         "A workspace was not found with name: %s\n" %
-                         nonexistent_name)
+                         "A workspace was not found with name: %s\n"
+                         % nonexistent_name)
+
+    def test_workspace_name_exists(self):
+        self.assertIsNone(self.workspace_manager._name_exists(self.name))
 
     def test_workspace_name_already_exists(self):
         duplicate_name = self.name
@@ -203,6 +307,11 @@
                          "A workspace already exists with name: %s.\n"
                          % duplicate_name)
 
+    def test_workspace_name_exists_check_new_name(self):
+        new_name = "fake_name"
+        self.assertIsNone(self.workspace_manager.
+                          _workspace_name_exists(new_name))
+
     def test_workspace_manager_path_not_exist(self):
         fake_path = "fake_path"
         with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
@@ -213,6 +322,11 @@
         self.assertEqual(mock_stdout.getvalue(),
                          "Path does not exist.\n")
 
+    def test_validate_path_exists(self):
+        new_path = self.path
+        self.assertIsNone(self.workspace_manager.
+                          _validate_path(new_path))
+
     def test_workspace_manager_list_workspaces(self):
         listed = self.workspace_manager.list_workspaces()
         self.assertEqual(1, len(listed))
@@ -242,3 +356,21 @@
         self.assertEqual(mock_stdout.getvalue(),
                          "None or empty path is specified for workspace."
                          " Please specify correct workspace path.\n")
+
+    def test_register_new_workspace_integer_data(self):
+        workspace_name = 12345
+        self.workspace_manager.register_new_workspace(
+            workspace_name, self.path)
+        self.assertIsNotNone(
+            self.workspace_manager.get_workspace(workspace_name))
+        self.assertEqual(
+            self.workspace_manager.get_workspace(workspace_name), self.path)
+
+    def test_register_new_workspace_alphanumeric_data(self):
+        workspace_name = 'abc123'
+        self.workspace_manager.register_new_workspace(
+            workspace_name, self.path)
+        self.assertIsNotNone(
+            self.workspace_manager.get_workspace(workspace_name))
+        self.assertEqual(
+            self.workspace_manager.get_workspace(workspace_name), self.path)
diff --git a/tempest/tests/lib/services/placement/__init__.py b/tempest/tests/lib/services/placement/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/tests/lib/services/placement/__init__.py
diff --git a/tempest/tests/lib/services/placement/test_placement_client.py b/tempest/tests/lib/services/placement/test_placement_client.py
new file mode 100644
index 0000000..1396a85
--- /dev/null
+++ b/tempest/tests/lib/services/placement/test_placement_client.py
@@ -0,0 +1,89 @@
+# Copyright (c) 2019 Ericsson
+#
+#    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.placement import placement_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestPlacementClient(base.BaseServiceTest):
+    FAKE_ALLOCATION_CANDIDATES = {
+        'allocation_requests': [
+            {'allocations': {
+                'rp-uuid': {'resources': {'VCPU': 42}}
+            }}
+        ],
+        'provider_summaries': {
+            'rp-uuid': {
+                'resources': {
+                    'VCPU': {'used': 0, 'capacity': 64},
+                    'MEMORY_MB': {'capacity': 11196, 'used': 0},
+                    'DISK_GB': {'capacity': 19, 'used': 0}
+                },
+                'traits': ["HW_CPU_X86_SVM"],
+            }
+        }
+    }
+
+    FAKE_ALLOCATIONS = {
+        'allocations': {
+            'rp-uuid-1': {
+                'resources': {
+                    'NET_BW_IGR_KILOBIT_PER_SEC': 1
+                },
+                'generation': 14
+            },
+            'rp-uuid2': {
+                'resources': {
+                    'MEMORY_MB': 256,
+                    'VCPU': 1
+                },
+                'generation': 9
+            }
+        }
+    }
+
+    def setUp(self):
+        super(TestPlacementClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = placement_client.PlacementClient(
+            fake_auth, 'placement', 'regionOne')
+
+    def _test_list_allocation_candidates(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_allocation_candidates,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_ALLOCATION_CANDIDATES,
+            to_utf=bytes_body,
+            **{'resources1': 'NET_BW_IGR_KILOBIT_PER_SEC:1'})
+
+    def test_list_allocation_candidates_with_str_body(self):
+        self._test_list_allocation_candidates()
+
+    def test_list_allocation_candidates_with_bytes_body(self):
+        self._test_list_allocation_candidates(bytes_body=True)
+
+    def _test_list_allocations(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_allocations,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_ALLOCATIONS,
+            to_utf=bytes_body,
+            **{'consumer_uuid': 'foo-bar'})
+
+    def test_list_allocations_with_str_body(self):
+        self._test_list_allocations()
+
+    def test_list_allocations_with_bytes_body(self):
+        self._test_list_allocations(bytes_body=True)