Merge "Log the result of command in get_nic_name_by_ip"
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/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_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_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/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..a6f5570 100644
--- a/tempest/lib/common/rest_client.py
+++ b/tempest/lib/common/rest_client.py
@@ -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..e7ac423 100644
--- a/tempest/lib/services/clients.py
+++ b/tempest/lib/services/clients.py
@@ -32,6 +32,7 @@
 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")
@@ -46,6 +47,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_basic_ops.py b/tempest/scenario/test_network_basic_ops.py
index c11070c..a473cc9 100644
--- a/tempest/scenario/test_network_basic_ops.py
+++ b/tempest/scenario/test_network_basic_ops.py
@@ -544,6 +544,7 @@
             should_connect=True, msg="after updating "
             "admin_state_up of router to True")
 
+    @decorators.skip_because(bug='1813198')
     @decorators.idempotent_id('d8bb918e-e2df-48b2-97cd-b73c95450980')
     @testtools.skipIf(CONF.network.shared_physical_network,
                       'network isolation not available')
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)