#    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 copy
import ddt
import time
from unittest import mock
from urllib import parse

import fixtures
from keystoneauth1 import exceptions as ks_exc
import os_resource_classes as orc
from oslo_serialization import jsonutils
from oslo_utils.fixture import uuidsentinel as uuids

import nova.conf
from nova import context
from nova import exception
from nova import objects
from nova.scheduler.client import report
from nova.scheduler import utils as scheduler_utils
from nova import test
from nova.tests import fixtures as nova_fixtures
from nova.tests.unit import fake_requests


CONF = nova.conf.CONF


class SafeConnectedTestCase(test.NoDBTestCase):
    """Test the safe_connect decorator for the scheduler client."""

    def setUp(self):
        super(SafeConnectedTestCase, self).setUp()
        self.context = context.get_admin_context()

        # need to mock this globally as SchedulerReportClient._create_client
        # is called again when EndpointNotFound is raised
        self.useFixture(
            fixtures.MonkeyPatch(
                'keystoneauth1.loading.load_auth_from_conf_options',
                mock.MagicMock()))

        self.client = report.SchedulerReportClient()

    @mock.patch('keystoneauth1.session.Session.request')
    def test_missing_endpoint(self, req):
        """Test EndpointNotFound behavior.

        A missing endpoint entry should not explode.
        """
        req.side_effect = ks_exc.EndpointNotFound()
        self.client._get_resource_provider(self.context, "fake")

        # reset the call count to demonstrate that future calls still
        # work
        req.reset_mock()
        self.client._get_resource_provider(self.context, "fake")
        self.assertTrue(req.called)

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_create_client')
    @mock.patch('keystoneauth1.session.Session.request')
    def test_missing_endpoint_create_client(self, req, create_client):
        """Test EndpointNotFound retry behavior.

        A missing endpoint should cause _create_client to be called.
        """
        req.side_effect = ks_exc.EndpointNotFound()
        self.client._get_resource_provider(self.context, "fake")

        # This is the second time _create_client is called, but the first since
        # the mock was created.
        self.assertTrue(create_client.called)

    @mock.patch('keystoneauth1.session.Session.request')
    def test_missing_auth(self, req):
        """Test Missing Auth handled correctly.

        A missing auth configuration should not explode.

        """
        req.side_effect = ks_exc.MissingAuthPlugin()
        self.client._get_resource_provider(self.context, "fake")

        # reset the call count to demonstrate that future calls still
        # work
        req.reset_mock()
        self.client._get_resource_provider(self.context, "fake")
        self.assertTrue(req.called)

    @mock.patch('keystoneauth1.session.Session.request')
    def test_unauthorized(self, req):
        """Test Unauthorized handled correctly.

        An unauthorized configuration should not explode.

        """
        req.side_effect = ks_exc.Unauthorized()
        self.client._get_resource_provider(self.context, "fake")

        # reset the call count to demonstrate that future calls still
        # work
        req.reset_mock()
        self.client._get_resource_provider(self.context, "fake")
        self.assertTrue(req.called)

    @mock.patch('keystoneauth1.session.Session.request')
    def test_connect_fail(self, req):
        """Test Connect Failure handled correctly.

        If we get a connect failure, this is transient, and we expect
        that this will end up working correctly later.

        """
        req.side_effect = ks_exc.ConnectFailure()
        self.client._get_resource_provider(self.context, "fake")

        # reset the call count to demonstrate that future calls do
        # work
        req.reset_mock()
        self.client._get_resource_provider(self.context, "fake")
        self.assertTrue(req.called)

    @mock.patch.object(report, 'LOG')
    def test_warning_limit(self, mock_log):
        # Assert that __init__ initializes _warn_count as we expect
        self.assertEqual(0, self.client._warn_count)
        mock_self = mock.MagicMock()
        mock_self._warn_count = 0
        for i in range(0, report.WARN_EVERY + 3):
            report.warn_limit(mock_self, 'warning')
        mock_log.warning.assert_has_calls([mock.call('warning'),
                                           mock.call('warning')])

    @mock.patch('keystoneauth1.session.Session.request')
    def test_failed_discovery(self, req):
        """Test DiscoveryFailure behavior.

        Failed discovery should not blow up.
        """
        req.side_effect = ks_exc.DiscoveryFailure()
        self.client._get_resource_provider(self.context, "fake")

        # reset the call count to demonstrate that future calls still
        # work
        req.reset_mock()
        self.client._get_resource_provider(self.context, "fake")
        self.assertTrue(req.called)


@ddt.ddt
class TestSingleton(test.NoDBTestCase):
    def test_singleton(self):
        # Make sure we start with a clean slate
        self.assertIsNone(report.PLACEMENTCLIENT)

        # Make sure the first call creates the singleton, sets it
        # globally, and returns it
        client = report.report_client_singleton()
        self.assertEqual(client, report.PLACEMENTCLIENT)

        # Make sure that a subsequent call returns the same thing
        # again and that the global is unchanged
        self.assertEqual(client, report.report_client_singleton())
        self.assertEqual(client, report.PLACEMENTCLIENT)

    @ddt.data(ks_exc.EndpointNotFound,
              ks_exc.MissingAuthPlugin,
              ks_exc.Unauthorized,
              ks_exc.DiscoveryFailure,
              ks_exc.ConnectFailure,
              ks_exc.RequestTimeout,
              ks_exc.GatewayTimeout,
              test.TestingException)
    def test_errors(self, exc):
        self._test_error(exc)

    @mock.patch.object(report, 'LOG')
    def _test_error(self, exc, mock_log):
        with mock.patch.object(report.SchedulerReportClient, '_create_client',
                               side_effect=exc):
            self.assertRaises(exc, report.report_client_singleton)
        mock_log.error.assert_called_once()

    def test_error_then_success(self):
        # Simulate an error
        self._test_error(ks_exc.ConnectFailure)

        # Ensure we did not set the global client
        self.assertIsNone(report.PLACEMENTCLIENT)

        # Call again, with no error
        client = report.report_client_singleton()

        # Make sure we got a client and that it was set as the global
        # one
        self.assertIsNotNone(client)
        self.assertEqual(client, report.PLACEMENTCLIENT)

        # Make sure we keep getting the same one
        client2 = report.report_client_singleton()
        self.assertEqual(client, client2)


class TestConstructor(test.NoDBTestCase):
    def setUp(self):
        super(TestConstructor, self).setUp()
        ksafx = self.useFixture(nova_fixtures.KSAFixture())
        self.load_auth_mock = ksafx.mock_load_auth
        self.load_sess_mock = ksafx.mock_load_sess

    def test_constructor(self):
        client = report.SchedulerReportClient()

        self.load_auth_mock.assert_called_once_with(CONF, 'placement')
        self.load_sess_mock.assert_called_once_with(
            CONF, 'placement', auth=self.load_auth_mock.return_value)
        self.assertEqual(['internal', 'public'], client._client.interface)
        self.assertEqual({'accept': 'application/json'},
                         client._client.additional_headers)

    def test_constructor_admin_interface(self):
        self.flags(valid_interfaces='admin', group='placement')
        client = report.SchedulerReportClient()

        self.load_auth_mock.assert_called_once_with(CONF, 'placement')
        self.load_sess_mock.assert_called_once_with(
            CONF, 'placement', auth=self.load_auth_mock.return_value)
        self.assertEqual(['admin'], client._client.interface)
        self.assertEqual({'accept': 'application/json'},
                         client._client.additional_headers)


class SchedulerReportClientTestCase(test.NoDBTestCase):

    def setUp(self):
        super(SchedulerReportClientTestCase, self).setUp()
        self.context = context.get_admin_context()
        self.useFixture(nova_fixtures.KSAFixture())
        self.ks_adap_mock = mock.Mock()
        self.compute_node = objects.ComputeNode(
            uuid=uuids.compute_node,
            hypervisor_hostname='foo',
            vcpus=8,
            cpu_allocation_ratio=16.0,
            memory_mb=1024,
            ram_allocation_ratio=1.5,
            local_gb=10,
            disk_allocation_ratio=1.0,
        )

        self.client = report.SchedulerReportClient(self.ks_adap_mock)

    def _init_provider_tree(self, generation_override=None,
                            resources_override=None):
        cn = self.compute_node
        resources = resources_override
        if resources_override is None:
            resources = {
                'VCPU': {
                    'total': cn.vcpus,
                    'reserved': 0,
                    'min_unit': 1,
                    'max_unit': cn.vcpus,
                    'step_size': 1,
                    'allocation_ratio': cn.cpu_allocation_ratio,
                },
                'MEMORY_MB': {
                    'total': cn.memory_mb,
                    'reserved': 512,
                    'min_unit': 1,
                    'max_unit': cn.memory_mb,
                    'step_size': 1,
                    'allocation_ratio': cn.ram_allocation_ratio,
                },
                'DISK_GB': {
                    'total': cn.local_gb,
                    'reserved': 0,
                    'min_unit': 1,
                    'max_unit': cn.local_gb,
                    'step_size': 1,
                    'allocation_ratio': cn.disk_allocation_ratio,
                },
            }
        generation = generation_override or 1
        rp_uuid = self.client._provider_tree.new_root(
            cn.hypervisor_hostname,
            cn.uuid,
            generation=generation,
        )
        self.client._provider_tree.update_inventory(rp_uuid, resources)

    def _validate_provider(self, name_or_uuid, **kwargs):
        """Validates existence and values of a provider in this client's
        _provider_tree.

        :param name_or_uuid: The name or UUID of the provider to validate.
        :param kwargs: Optional keyword arguments of ProviderData attributes
                       whose values are to be validated.
        """
        found = self.client._provider_tree.data(name_or_uuid)
        # If kwargs provided, their names indicate ProviderData attributes
        for attr, expected in kwargs.items():
            try:
                self.assertEqual(getattr(found, attr), expected)
            except AttributeError:
                self.fail("Provider with name or UUID %s doesn't have "
                          "attribute %s (expected value: %s)" %
                          (name_or_uuid, attr, expected))


class TestPutAllocations(SchedulerReportClientTestCase):
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.put')
    def test_put_allocations(self, mock_put):
        mock_put.return_value.status_code = 204
        mock_put.return_value.text = "cool"
        rp_uuid = mock.sentinel.rp
        consumer_uuid = mock.sentinel.consumer
        data = {"MEMORY_MB": 1024}
        expected_url = "/allocations/%s" % consumer_uuid
        payload = {
            "allocations": {
                rp_uuid: {"resources": data}
            },
            "project_id": mock.sentinel.project_id,
            "user_id": mock.sentinel.user_id,
            "consumer_generation": mock.sentinel.consumer_generation
        }
        resp = self.client.put_allocations(
            self.context, consumer_uuid, payload)
        self.assertTrue(resp)
        mock_put.assert_called_once_with(
            expected_url, payload, version='1.28',
            global_request_id=self.context.global_id)

    @mock.patch.object(report.LOG, 'warning')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.put')
    def test_put_allocations_fail(self, mock_put, mock_warn):
        mock_put.return_value.status_code = 400
        mock_put.return_value.text = "not cool"
        rp_uuid = mock.sentinel.rp
        consumer_uuid = mock.sentinel.consumer
        data = {"MEMORY_MB": 1024}
        expected_url = "/allocations/%s" % consumer_uuid
        payload = {
            "allocations": {
                rp_uuid: {"resources": data}
            },
            "project_id": mock.sentinel.project_id,
            "user_id": mock.sentinel.user_id,
            "consumer_generation": mock.sentinel.consumer_generation
        }
        resp = self.client.put_allocations(
            self.context, consumer_uuid, payload)

        self.assertFalse(resp)
        mock_put.assert_called_once_with(
            expected_url, payload, version='1.28',
            global_request_id=self.context.global_id)
        log_msg = mock_warn.call_args[0][0]
        self.assertIn("Failed to save allocation for", log_msg)

    def test_put_allocations_fail_connection_error(self):
        self.ks_adap_mock.put.side_effect = ks_exc.EndpointNotFound()
        self.assertRaises(
            exception.PlacementAPIConnectFailure, self.client.put_allocations,
            self.context, mock.sentinel.consumer, mock.sentinel.payload)

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.put')
    def test_put_allocations_fail_due_to_consumer_generation_conflict(
            self, mock_put):
        mock_put.return_value = fake_requests.FakeResponse(
            status_code=409,
            content=jsonutils.dumps(
                {'errors': [{'code': 'placement.concurrent_update',
                             'detail': 'consumer generation conflict'}]}))

        rp_uuid = mock.sentinel.rp
        consumer_uuid = mock.sentinel.consumer
        data = {"MEMORY_MB": 1024}
        expected_url = "/allocations/%s" % consumer_uuid
        payload = {
            "allocations": {
                rp_uuid: {"resources": data}
            },
            "project_id": mock.sentinel.project_id,
            "user_id": mock.sentinel.user_id,
            "consumer_generation": mock.sentinel.consumer_generation
        }
        self.assertRaises(exception.AllocationUpdateFailed,
                          self.client.put_allocations,
                          self.context, consumer_uuid, payload)

        mock_put.assert_called_once_with(
            expected_url, mock.ANY, version='1.28',
            global_request_id=self.context.global_id)

    @mock.patch('time.sleep', new=mock.Mock())
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.put')
    def test_put_allocations_retries_conflict(self, mock_put):
        failed = fake_requests.FakeResponse(
            status_code=409,
            content=jsonutils.dumps(
                {'errors': [{'code': 'placement.concurrent_update',
                             'detail': ''}]}))

        succeeded = mock.MagicMock()
        succeeded.status_code = 204

        mock_put.side_effect = (failed, succeeded)

        rp_uuid = mock.sentinel.rp
        consumer_uuid = mock.sentinel.consumer
        data = {"MEMORY_MB": 1024}
        expected_url = "/allocations/%s" % consumer_uuid
        payload = {
            "allocations": {
                rp_uuid: {"resources": data}
            },
            "project_id": mock.sentinel.project_id,
            "user_id": mock.sentinel.user_id,
            "consumer_generation": mock.sentinel.consumer_generation
        }
        resp = self.client.put_allocations(
            self.context, consumer_uuid, payload)
        self.assertTrue(resp)
        mock_put.assert_has_calls([
            mock.call(expected_url, payload, version='1.28',
                      global_request_id=self.context.global_id)] * 2)

    @mock.patch('time.sleep', new=mock.Mock())
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.put')
    def test_put_allocations_retry_gives_up(self, mock_put):

        failed = fake_requests.FakeResponse(
            status_code=409,
            content=jsonutils.dumps(
                {'errors': [{'code': 'placement.concurrent_update',
                             'detail': ''}]}))

        mock_put.return_value = failed

        rp_uuid = mock.sentinel.rp
        consumer_uuid = mock.sentinel.consumer
        data = {"MEMORY_MB": 1024}
        expected_url = "/allocations/%s" % consumer_uuid
        payload = {
            "allocations": {
                rp_uuid: {"resources": data}
            },
            "project_id": mock.sentinel.project_id,
            "user_id": mock.sentinel.user_id,
            "consumer_generation": mock.sentinel.consumer_generation
        }
        resp = self.client.put_allocations(
            self.context, consumer_uuid, payload)
        self.assertFalse(resp)
        mock_put.assert_has_calls([
            mock.call(expected_url, payload, version='1.28',
            global_request_id=self.context.global_id)] * 3)

    def test_claim_resources_success(self):
        get_resp_mock = mock.Mock(status_code=200)
        get_resp_mock.json.return_value = {
            'allocations': {},  # build instance, not move
        }
        self.ks_adap_mock.get.return_value = get_resp_mock
        resp_mock = mock.Mock(status_code=204)
        self.ks_adap_mock.put.return_value = resp_mock
        consumer_uuid = uuids.consumer_uuid
        alloc_req = {
            'allocations': {
                uuids.cn1: {
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    }
                },
            },
        }

        project_id = uuids.project_id
        user_id = uuids.user_id
        res = self.client.claim_resources(self.context, consumer_uuid,
                                          alloc_req, project_id, user_id,
                                          allocation_request_version='1.12')

        expected_url = "/allocations/%s" % consumer_uuid
        expected_payload = {'allocations': {
            rp_uuid: alloc
                for rp_uuid, alloc in alloc_req['allocations'].items()}}
        expected_payload['project_id'] = project_id
        expected_payload['user_id'] = user_id
        self.ks_adap_mock.put.assert_called_once_with(
            expected_url, microversion='1.12', json=expected_payload,
            global_request_id=self.context.global_id)

        self.assertTrue(res)

    def test_claim_resources_older_alloc_req(self):
        """Test the case when a stale allocation request is sent to the report
        client to claim
        """
        get_resp_mock = mock.Mock(status_code=200)
        get_resp_mock.json.return_value = {
            'allocations': {},  # build instance, not move
        }
        self.ks_adap_mock.get.return_value = get_resp_mock
        resp_mock = mock.Mock(status_code=204)
        self.ks_adap_mock.put.return_value = resp_mock
        consumer_uuid = uuids.consumer_uuid
        alloc_req = {
            'allocations': {
                uuids.cn1: {
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    }
                },
            },
        }

        project_id = uuids.project_id
        user_id = uuids.user_id
        res = self.client.claim_resources(self.context, consumer_uuid,
                                          alloc_req, project_id, user_id,
                                          allocation_request_version='1.12')

        expected_url = "/allocations/%s" % consumer_uuid
        expected_payload = {
            'allocations': {
                rp_uuid: res
                for rp_uuid, res in alloc_req['allocations'].items()},
            # no consumer generation in the payload as the caller requested
            # older microversion to be used
            'project_id': project_id,
            'user_id': user_id}
        self.ks_adap_mock.put.assert_called_once_with(
            expected_url, microversion='1.12', json=expected_payload,
            global_request_id=self.context.global_id)
        self.assertTrue(res)

    def test_claim_resources_success_resize_to_same_host_no_shared(self):
        """Tests resize to the same host operation. In this case allocation
        exists against the same host RP but with the migration_uuid.
        """
        get_current_allocations_resp_mock = mock.Mock(status_code=200)
        # source host allocation held by the migration_uuid so it is not
        # not returned to the claim code as that asks for the instance_uuid
        # consumer
        get_current_allocations_resp_mock.json.return_value = {
            'allocations': {},
            "consumer_generation": 1,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id
        }

        self.ks_adap_mock.get.return_value = get_current_allocations_resp_mock
        put_allocations_resp_mock = mock.Mock(status_code=204)
        self.ks_adap_mock.put.return_value = put_allocations_resp_mock
        consumer_uuid = uuids.consumer_uuid
        # This is the resize-up allocation where VCPU, MEMORY_MB and DISK_GB
        # are all being increased but on the same host. We also throw a custom
        # resource class in the new allocation to make sure it's not lost
        alloc_req = {
            'allocations': {
                uuids.same_host: {
                    'resources': {
                        'VCPU': 2,
                        'MEMORY_MB': 2048,
                        'DISK_GB': 40,
                        'CUSTOM_FOO': 1
                    }
                },
            },
            # this allocation request comes from the scheduler therefore it
            # does not have consumer_generation in it.
            "project_id": uuids.project_id,
            "user_id": uuids.user_id
        }

        project_id = uuids.project_id
        user_id = uuids.user_id
        res = self.client.claim_resources(self.context, consumer_uuid,
                                          alloc_req, project_id, user_id,
                                          allocation_request_version='1.28')

        expected_url = "/allocations/%s" % consumer_uuid
        expected_payload = {
            'allocations': {
                uuids.same_host: {
                    'resources': {
                        'VCPU': 2,
                        'MEMORY_MB': 2048,
                        'DISK_GB': 40,
                        'CUSTOM_FOO': 1
                    }
                },
            },
            # report client assumes a new consumer in this case
            'consumer_generation': None,
            'project_id': project_id,
            'user_id': user_id}
        self.ks_adap_mock.put.assert_called_once_with(
            expected_url, microversion='1.28', json=mock.ANY,
            global_request_id=self.context.global_id)
        # We have to pull the json body from the mock call_args to validate
        # it separately otherwise hash seed issues get in the way.
        actual_payload = self.ks_adap_mock.put.call_args[1]['json']
        self.assertEqual(expected_payload, actual_payload)

        self.assertTrue(res)

    def test_claim_resources_success_resize_to_same_host_with_shared(self):
        """Tests resize to the same host operation. In this case allocation
        exists against the same host RP and the shared RP but with the
        migration_uuid.
        """
        get_current_allocations_resp_mock = mock.Mock(status_code=200)
        # source host allocation held by the migration_uuid so it is not
        # not returned to the claim code as that asks for the instance_uuid
        # consumer
        get_current_allocations_resp_mock.json.return_value = {
            'allocations': {},
            "consumer_generation": 1,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id
        }

        self.ks_adap_mock.get.return_value = get_current_allocations_resp_mock
        put_allocations_resp_mock = mock.Mock(status_code=204)
        self.ks_adap_mock.put.return_value = put_allocations_resp_mock
        consumer_uuid = uuids.consumer_uuid
        # This is the resize-up allocation where VCPU, MEMORY_MB and DISK_GB
        # are all being increased but on the same host. We also throw a custom
        # resource class in the new allocation to make sure it's not lost
        alloc_req = {
            'allocations': {
                uuids.same_host: {
                    'resources': {
                        'VCPU': 2,
                        'MEMORY_MB': 2048,
                        'CUSTOM_FOO': 1
                    }
                },
                uuids.shared_storage: {
                    'resources': {
                        'DISK_GB': 40,
                    }
                },
            },
            # this allocation request comes from the scheduler therefore it
            # does not have consumer_generation in it.
            "project_id": uuids.project_id,
            "user_id": uuids.user_id
        }

        project_id = uuids.project_id
        user_id = uuids.user_id
        res = self.client.claim_resources(self.context, consumer_uuid,
                                          alloc_req, project_id, user_id,
                                          allocation_request_version='1.28')

        expected_url = "/allocations/%s" % consumer_uuid
        expected_payload = {
            'allocations': {
                uuids.same_host: {
                    'resources': {
                        'VCPU': 2,
                        'MEMORY_MB': 2048,
                        'CUSTOM_FOO': 1
                    }
                },
                uuids.shared_storage: {
                    'resources': {
                        'DISK_GB': 40,
                    }
                },
            },
            # report client assumes a new consumer in this case
            'consumer_generation': None,
            'project_id': project_id,
            'user_id': user_id}
        self.ks_adap_mock.put.assert_called_once_with(
            expected_url, microversion='1.28', json=mock.ANY,
            global_request_id=self.context.global_id)
        # We have to pull the json body from the mock call_args to validate
        # it separately otherwise hash seed issues get in the way.
        actual_payload = self.ks_adap_mock.put.call_args[1]['json']
        self.assertEqual(expected_payload, actual_payload)

        self.assertTrue(res)

    def test_claim_resources_success_evacuate_no_shared(self):
        """Tests non-forced evacuate. In this case both the source and the
        dest allocation are held by the instance_uuid in placement. So the
        claim code needs to merge allocations. The second claim comes from the
        scheduler and therefore it does not have consumer_generation in it.
        """
        # the source allocation is also held by the instance_uuid so report
        # client will see it.
        current_allocs = {
            'allocations': {
                uuids.source_host: {
                    'generation': 42,
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                        'DISK_GB': 20
                    },
                },
            },
            "consumer_generation": 1,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id
        }
        self.ks_adap_mock.get.return_value = fake_requests.FakeResponse(
            status_code=200,
            content=jsonutils.dumps(current_allocs))
        put_allocations_resp_mock = fake_requests.FakeResponse(status_code=204)
        self.ks_adap_mock.put.return_value = put_allocations_resp_mock
        consumer_uuid = uuids.consumer_uuid
        # this is an evacuate so we have the same resources request towards the
        # dest host
        alloc_req = {
            'allocations': {
                uuids.dest_host: {
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                        'DISK_GB': 20,
                    }
                },
            },
            # this allocation request comes from the scheduler therefore it
            # does not have consumer_generation in it.
            "project_id": uuids.project_id,
            "user_id": uuids.user_id
        }

        project_id = uuids.project_id
        user_id = uuids.user_id
        res = self.client.claim_resources(self.context, consumer_uuid,
                                          alloc_req, project_id, user_id,
                                          allocation_request_version='1.28')

        expected_url = "/allocations/%s" % consumer_uuid
        # we expect that both the source and dest allocations are here
        expected_payload = {
            'allocations': {
                uuids.source_host: {
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                        'DISK_GB': 20
                    },
                },
                uuids.dest_host: {
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                        'DISK_GB': 20,
                    }
                },
            },
            # report client uses the consumer_generation that it got from
            # placement when asked for the existing allocations
            'consumer_generation': 1,
            'project_id': project_id,
            'user_id': user_id}
        self.ks_adap_mock.put.assert_called_once_with(
            expected_url, microversion='1.28', json=mock.ANY,
            global_request_id=self.context.global_id)
        # We have to pull the json body from the mock call_args to validate
        # it separately otherwise hash seed issues get in the way.
        actual_payload = self.ks_adap_mock.put.call_args[1]['json']
        self.assertEqual(expected_payload, actual_payload)

        self.assertTrue(res)

    def test_claim_resources_success_evacuate_with_shared(self):
        """Similar test that test_claim_resources_success_evacuate_no_shared
        but adds shared disk into the mix.
        """
        # the source allocation is also held by the instance_uuid so report
        # client will see it.
        current_allocs = {
            'allocations': {
                uuids.source_host: {
                    'generation': 42,
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    },
                },
                uuids.shared_storage: {
                    'generation': 42,
                    'resources': {
                        'DISK_GB': 20,
                    },
                },
            },
            "consumer_generation": 1,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id
        }
        self.ks_adap_mock.get.return_value = fake_requests.FakeResponse(
            status_code=200,
            content = jsonutils.dumps(current_allocs))
        self.ks_adap_mock.put.return_value = fake_requests.FakeResponse(
            status_code=204)
        consumer_uuid = uuids.consumer_uuid
        # this is an evacuate so we have the same resources request towards the
        # dest host
        alloc_req = {
            'allocations': {
                uuids.dest_host: {
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    },
                },
                uuids.shared_storage: {
                    'generation': 42,
                    'resources': {
                        'DISK_GB': 20,
                    },
                },
            },
            # this allocation request comes from the scheduler therefore it
            # does not have consumer_generation in it.
            "project_id": uuids.project_id,
            "user_id": uuids.user_id
        }

        project_id = uuids.project_id
        user_id = uuids.user_id
        res = self.client.claim_resources(self.context, consumer_uuid,
                                          alloc_req, project_id, user_id,
                                          allocation_request_version='1.28')

        expected_url = "/allocations/%s" % consumer_uuid
        # we expect that both the source and dest allocations are here plus the
        # shared storage allocation
        expected_payload = {
            'allocations': {
                uuids.source_host: {
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    },
                },
                uuids.dest_host: {
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    }
                },
                uuids.shared_storage: {
                    'resources': {
                        'DISK_GB': 20,
                    },
                },
            },
            # report client uses the consumer_generation that got from
            # placement when asked for the existing allocations
            'consumer_generation': 1,
            'project_id': project_id,
            'user_id': user_id}
        self.ks_adap_mock.put.assert_called_once_with(
            expected_url, microversion='1.28', json=mock.ANY,
            global_request_id=self.context.global_id)
        # We have to pull the json body from the mock call_args to validate
        # it separately otherwise hash seed issues get in the way.
        actual_payload = self.ks_adap_mock.put.call_args[1]['json']
        self.assertEqual(expected_payload, actual_payload)

        self.assertTrue(res)

    def test_claim_resources_success_force_evacuate_no_shared(self):
        """Tests forced evacuate. In this case both the source and the
        dest allocation are held by the instance_uuid in placement. So the
        claim code needs to merge allocations. The second claim comes from the
        conductor and therefore it does have consumer_generation in it.
        """
        # the source allocation is also held by the instance_uuid so report
        # client will see it.
        current_allocs = {
            'allocations': {
                uuids.source_host: {
                    'generation': 42,
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                        'DISK_GB': 20
                    },
                },
            },
            "consumer_generation": 1,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id
        }

        self.ks_adap_mock.get.return_value = fake_requests.FakeResponse(
            status_code=200,
            content=jsonutils.dumps(current_allocs))
        self.ks_adap_mock.put.return_value = fake_requests.FakeResponse(
            status_code=204)
        consumer_uuid = uuids.consumer_uuid
        # this is an evacuate so we have the same resources request towards the
        # dest host
        alloc_req = {
            'allocations': {
                uuids.dest_host: {
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                        'DISK_GB': 20,
                    }
                },
            },
            # this allocation request comes from the conductor that read the
            # allocation from placement therefore it has consumer_generation in
            # it.
            "consumer_generation": 1,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id
        }

        project_id = uuids.project_id
        user_id = uuids.user_id
        res = self.client.claim_resources(self.context, consumer_uuid,
                                          alloc_req, project_id, user_id,
                                          allocation_request_version='1.28')

        expected_url = "/allocations/%s" % consumer_uuid
        # we expect that both the source and dest allocations are here
        expected_payload = {
            'allocations': {
                uuids.source_host: {
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                        'DISK_GB': 20
                    },
                },
                uuids.dest_host: {
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                        'DISK_GB': 20,
                    }
                },
            },
            # report client uses the consumer_generation that it got in the
            # allocation request
            'consumer_generation': 1,
            'project_id': project_id,
            'user_id': user_id}
        self.ks_adap_mock.put.assert_called_once_with(
            expected_url, microversion='1.28', json=mock.ANY,
            global_request_id=self.context.global_id)
        # We have to pull the json body from the mock call_args to validate
        # it separately otherwise hash seed issues get in the way.
        actual_payload = self.ks_adap_mock.put.call_args[1]['json']
        self.assertEqual(expected_payload, actual_payload)

        self.assertTrue(res)

    def test_claim_resources_success_force_evacuate_with_shared(self):
        """Similar test that
        test_claim_resources_success_force_evacuate_no_shared but adds shared
        disk into the mix.
        """
        # the source allocation is also held by the instance_uuid so report
        # client will see it.
        current_allocs = {
            'allocations': {
                uuids.source_host: {
                    'generation': 42,
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    },
                },
                uuids.shared_storage: {
                    'generation': 42,
                    'resources': {
                        'DISK_GB': 20,
                    },
                },
            },
            "consumer_generation": 1,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id
        }

        self.ks_adap_mock.get.return_value = fake_requests.FakeResponse(
            status_code=200,
            content=jsonutils.dumps(current_allocs))
        self.ks_adap_mock.put.return_value = fake_requests.FakeResponse(
            status_code=204)
        consumer_uuid = uuids.consumer_uuid
        # this is an evacuate so we have the same resources request towards the
        # dest host
        alloc_req = {
            'allocations': {
                uuids.dest_host: {
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    },
                },
                uuids.shared_storage: {
                    'generation': 42,
                    'resources': {
                        'DISK_GB': 20,
                    },
                },
            },
            # this allocation request comes from the conductor that read the
            # allocation from placement therefore it has consumer_generation in
            # it.
            "consumer_generation": 1,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id
        }

        project_id = uuids.project_id
        user_id = uuids.user_id
        res = self.client.claim_resources(self.context, consumer_uuid,
                                          alloc_req, project_id, user_id,
                                          allocation_request_version='1.28')

        expected_url = "/allocations/%s" % consumer_uuid
        # we expect that both the source and dest allocations are here plus the
        # shared storage allocation
        expected_payload = {
            'allocations': {
                uuids.source_host: {
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    },
                },
                uuids.dest_host: {
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    }
                },
                uuids.shared_storage: {
                    'resources': {
                        'DISK_GB': 20,
                    },
                },
            },
            # report client uses the consumer_generation that it got in the
            # allocation request
            'consumer_generation': 1,
            'project_id': project_id,
            'user_id': user_id}
        self.ks_adap_mock.put.assert_called_once_with(
            expected_url, microversion='1.28', json=mock.ANY,
            global_request_id=self.context.global_id)
        # We have to pull the json body from the mock call_args to validate
        # it separately otherwise hash seed issues get in the way.
        actual_payload = self.ks_adap_mock.put.call_args[1]['json']
        self.assertEqual(expected_payload, actual_payload)

        self.assertTrue(res)

    @mock.patch('time.sleep', new=mock.Mock())
    def test_claim_resources_fail_due_to_rp_generation_retry_success(self):
        get_resp_mock = mock.Mock(status_code=200)
        get_resp_mock.json.return_value = {
            'allocations': {},  # build instance, not move
        }
        self.ks_adap_mock.get.return_value = get_resp_mock
        resp_mocks = [
            fake_requests.FakeResponse(
                409,
                jsonutils.dumps(
                    {'errors': [
                        {'code': 'placement.concurrent_update',
                         'detail': ''}]})),
            fake_requests.FakeResponse(204)
        ]
        self.ks_adap_mock.put.side_effect = resp_mocks
        consumer_uuid = uuids.consumer_uuid
        alloc_req = {
            'allocations': {
                uuids.cn1: {
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    }
                },
            },
        }

        project_id = uuids.project_id
        user_id = uuids.user_id
        res = self.client.claim_resources(self.context, consumer_uuid,
                                          alloc_req, project_id, user_id,
                                          allocation_request_version='1.28')

        expected_url = "/allocations/%s" % consumer_uuid
        expected_payload = {
            'allocations':
                {rp_uuid: res
                    for rp_uuid, res in alloc_req['allocations'].items()}
        }
        expected_payload['project_id'] = project_id
        expected_payload['user_id'] = user_id
        expected_payload['consumer_generation'] = None
        # We should have exactly two calls to the placement API that look
        # identical since we're retrying the same HTTP request
        expected_calls = [
            mock.call(expected_url, microversion='1.28', json=expected_payload,
                      global_request_id=self.context.global_id)] * 2
        self.assertEqual(len(expected_calls),
                         self.ks_adap_mock.put.call_count)
        self.ks_adap_mock.put.assert_has_calls(expected_calls)

        self.assertTrue(res)

    @mock.patch.object(report.LOG, 'warning')
    def test_claim_resources_failure(self, mock_log):
        get_resp_mock = mock.Mock(status_code=200)
        get_resp_mock.json.return_value = {
            'allocations': {},  # build instance, not move
        }
        self.ks_adap_mock.get.return_value = get_resp_mock
        resp_mock = fake_requests.FakeResponse(
            409,
            jsonutils.dumps(
                {'errors': [
                    {'code': 'something else',
                     'detail': 'not cool'}]}))

        self.ks_adap_mock.put.return_value = resp_mock
        consumer_uuid = uuids.consumer_uuid
        alloc_req = {
            'allocations': {
                uuids.cn1: {
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    }
                },
            },
        }

        project_id = uuids.project_id
        user_id = uuids.user_id
        res = self.client.claim_resources(self.context, consumer_uuid,
                                          alloc_req, project_id, user_id,
                                          allocation_request_version='1.28')

        expected_url = "/allocations/%s" % consumer_uuid
        expected_payload = {
            'allocations':
                {rp_uuid: res
                    for rp_uuid, res in alloc_req['allocations'].items()}
        }
        expected_payload['project_id'] = project_id
        expected_payload['user_id'] = user_id
        expected_payload['consumer_generation'] = None
        self.ks_adap_mock.put.assert_called_once_with(
            expected_url, microversion='1.28', json=expected_payload,
            global_request_id=self.context.global_id)

        self.assertFalse(res)
        self.assertTrue(mock_log.called)

    def test_claim_resources_consumer_generation_failure(self):
        get_resp_mock = mock.Mock(status_code=200)
        get_resp_mock.json.return_value = {
            'allocations': {},  # build instance, not move
        }
        self.ks_adap_mock.get.return_value = get_resp_mock
        resp_mock = fake_requests.FakeResponse(
            409,
            jsonutils.dumps(
                {'errors': [
                    {'code': 'placement.concurrent_update',
                     'detail': 'consumer generation conflict'}]}))

        self.ks_adap_mock.put.return_value = resp_mock
        consumer_uuid = uuids.consumer_uuid
        alloc_req = {
            'allocations': {
                uuids.cn1: {
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    }
                },
            },
        }

        project_id = uuids.project_id
        user_id = uuids.user_id
        self.assertRaises(exception.AllocationUpdateFailed,
                          self.client.claim_resources, self.context,
                          consumer_uuid, alloc_req, project_id, user_id,
                          allocation_request_version='1.28')

        expected_url = "/allocations/%s" % consumer_uuid
        expected_payload = {
            'allocations': {
                rp_uuid: res
                for rp_uuid, res in alloc_req['allocations'].items()},
            'project_id': project_id,
            'user_id': user_id,
            'consumer_generation': None}
        self.ks_adap_mock.put.assert_called_once_with(
            expected_url, microversion='1.28', json=expected_payload,
            global_request_id=self.context.global_id)

    def test_remove_provider_from_inst_alloc_no_shared(self):
        """Tests that the method which manipulates an existing doubled-up
        allocation for a move operation to remove the source host results in
        sending placement the proper payload to PUT
        /allocations/{consumer_uuid} call.
        """
        get_resp_mock = mock.Mock(status_code=200)
        get_resp_mock.json.side_effect = [
            {
            'allocations': {
                uuids.source: {
                    'resource_provider_generation': 42,
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    },
                },
                uuids.destination: {
                    'resource_provider_generation': 42,
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    },
                },
            },
            'consumer_generation': 1,
            'project_id': uuids.project_id,
            'user_id': uuids.user_id,
            },
            # the second get is for resource providers in the compute tree,
            # return just the compute
            {
                "resource_providers": [
                    {
                        "uuid": uuids.source,
                    },
                ]
            },
        ]
        self.ks_adap_mock.get.return_value = get_resp_mock
        resp_mock = mock.Mock(status_code=204)
        self.ks_adap_mock.put.return_value = resp_mock
        consumer_uuid = uuids.consumer_uuid
        project_id = uuids.project_id
        user_id = uuids.user_id
        res = self.client.remove_provider_tree_from_instance_allocation(
            self.context, consumer_uuid, uuids.source)

        expected_url = "/allocations/%s" % consumer_uuid
        # New allocations should only include the destination...
        expected_payload = {
            'allocations': {
                uuids.destination: {
                    'resource_provider_generation': 42,
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    },
                },
            },
            'consumer_generation': 1,
            'project_id': project_id,
            'user_id': user_id
        }
        # We have to pull the json body from the mock call_args to validate
        # it separately otherwise hash seed issues get in the way.
        actual_payload = self.ks_adap_mock.put.call_args[1]['json']
        self.assertEqual(expected_payload, actual_payload)
        self.ks_adap_mock.put.assert_called_once_with(
            expected_url, microversion='1.28', json=mock.ANY,
            global_request_id=self.context.global_id)

        self.assertTrue(res)

    def test_remove_provider_from_inst_alloc_with_shared(self):
        """Tests that the method which manipulates an existing doubled-up
        allocation with DISK_GB being consumed from a shared storage provider
        for a move operation to remove the source host results in sending
        placement the proper payload to PUT /allocations/{consumer_uuid}
        call.
        """
        get_resp_mock = mock.Mock(status_code=200)
        get_resp_mock.json.side_effect = [
            {
            'allocations': {
                uuids.source: {
                    'resource_provider_generation': 42,
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    },
                },
                uuids.shared_storage: {
                    'resource_provider_generation': 42,
                    'resources': {
                        'DISK_GB': 100,
                    },
                },
                uuids.destination: {
                    'resource_provider_generation': 42,
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    },
                },
            },
            'consumer_generation': 1,
            'project_id': uuids.project_id,
            'user_id': uuids.user_id,
            },
            # the second get is for resource providers in the compute tree,
            # return just the compute
            {
                "resource_providers": [
                    {
                        "uuid": uuids.source,
                    },
                ]
            },
        ]
        self.ks_adap_mock.get.return_value = get_resp_mock
        resp_mock = mock.Mock(status_code=204)
        self.ks_adap_mock.put.return_value = resp_mock
        consumer_uuid = uuids.consumer_uuid
        project_id = uuids.project_id
        user_id = uuids.user_id
        res = self.client.remove_provider_tree_from_instance_allocation(
            self.context, consumer_uuid, uuids.source)

        expected_url = "/allocations/%s" % consumer_uuid
        # New allocations should only include the destination...
        expected_payload = {
            'allocations': {
                uuids.shared_storage: {
                    'resource_provider_generation': 42,
                    'resources': {
                        'DISK_GB': 100,
                    },
                },
                uuids.destination: {
                    'resource_provider_generation': 42,
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    },
                },
            },
            'consumer_generation': 1,
            'project_id': project_id,
            'user_id': user_id
        }
        # We have to pull the json body from the mock call_args to validate
        # it separately otherwise hash seed issues get in the way.
        actual_payload = self.ks_adap_mock.put.call_args[1]['json']
        self.assertEqual(expected_payload, actual_payload)
        self.ks_adap_mock.put.assert_called_once_with(
            expected_url, microversion='1.28', json=mock.ANY,
            global_request_id=self.context.global_id)

        self.assertTrue(res)

    def test_remove_provider_from_inst_alloc_no_source(self):
        """Tests that if remove_provider_tree_from_instance_allocation() fails
        to find any allocations for the source host, it just returns True and
        does not attempt to rewrite the allocation for the consumer.
        """
        get_resp_mock = mock.Mock(status_code=200)
        get_resp_mock.json.side_effect = [
            # Act like the allocations already did not include the source host
            # for some reason
            {
            'allocations': {
                uuids.shared_storage: {
                    'resource_provider_generation': 42,
                    'resources': {
                        'DISK_GB': 100,
                    },
                },
                uuids.destination: {
                    'resource_provider_generation': 42,
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    },
                },
            },
            'consumer_generation': 1,
            'project_id': uuids.project_id,
            'user_id': uuids.user_id,
            },
            # the second get is for resource providers in the compute tree,
            # return just the compute
            {
                "resource_providers": [
                    {
                        "uuid": uuids.source,
                    },
                ]
            },
        ]
        self.ks_adap_mock.get.return_value = get_resp_mock
        consumer_uuid = uuids.consumer_uuid
        res = self.client.remove_provider_tree_from_instance_allocation(
            self.context, consumer_uuid, uuids.source)

        self.ks_adap_mock.get.assert_called()
        self.ks_adap_mock.put.assert_not_called()

        self.assertTrue(res)

    def test_remove_provider_from_inst_alloc_fail_get_allocs(self):
        self.ks_adap_mock.get.return_value = fake_requests.FakeResponse(
            status_code=500)
        consumer_uuid = uuids.consumer_uuid
        self.assertRaises(
            exception.ConsumerAllocationRetrievalFailed,
            self.client.remove_provider_tree_from_instance_allocation,
            self.context, consumer_uuid, uuids.source)

        self.ks_adap_mock.get.assert_called()
        self.ks_adap_mock.put.assert_not_called()

    def test_remove_provider_from_inst_alloc_consumer_gen_conflict(self):
        get_resp_mock = mock.Mock(status_code=200)
        get_resp_mock.json.side_effect = [
            {
            'allocations': {
                uuids.source: {
                    'resource_provider_generation': 42,
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    },
                },
                uuids.destination: {
                    'resource_provider_generation': 42,
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    },
                },
            },
            'consumer_generation': 1,
            'project_id': uuids.project_id,
            'user_id': uuids.user_id,
            },
            # the second get is for resource providers in the compute tree,
            # return just the compute
            {
                "resource_providers": [
                    {
                        "uuid": uuids.source,
                    },
                ]
            },
        ]
        self.ks_adap_mock.get.return_value = get_resp_mock
        resp_mock = mock.Mock(status_code=409)
        self.ks_adap_mock.put.return_value = resp_mock
        consumer_uuid = uuids.consumer_uuid
        res = self.client.remove_provider_tree_from_instance_allocation(
            self.context, consumer_uuid, uuids.source)

        self.assertFalse(res)

    def test_remove_provider_tree_from_inst_alloc_nested(self):
        self.ks_adap_mock.get.side_effect = [
            fake_requests.FakeResponse(
                status_code=200,
                content=jsonutils.dumps(
                    {
                        'allocations': {
                            uuids.source_compute: {
                                'resource_provider_generation': 42,
                                'resources': {
                                    'VCPU': 1,
                                    'MEMORY_MB': 1024,
                                },
                            },
                            uuids.source_nested: {
                                'resource_provider_generation': 42,
                                'resources': {
                                    'CUSTOM_MAGIC': 1
                                },
                            },
                            uuids.destination: {
                                'resource_provider_generation': 42,
                                'resources': {
                                    'VCPU': 1,
                                    'MEMORY_MB': 1024,
                                },
                            },
                        },
                        'consumer_generation': 1,
                        'project_id': uuids.project_id,
                        'user_id': uuids.user_id,
                    })),
            # the second get is for resource providers in the compute tree,
            # return both RPs in the source compute tree
            fake_requests.FakeResponse(
                status_code=200,
                content=jsonutils.dumps(
                    {
                        "resource_providers": [
                            {
                                "uuid": uuids.source_compute,
                            },
                            {
                                "uuid": uuids.source_nested,
                            },
                        ]
                    }))
        ]
        self.ks_adap_mock.put.return_value = fake_requests.FakeResponse(
            status_code=204)
        consumer_uuid = uuids.consumer_uuid
        project_id = uuids.project_id
        user_id = uuids.user_id
        res = self.client.remove_provider_tree_from_instance_allocation(
            self.context, consumer_uuid, uuids.source_compute)

        expected_url = "/allocations/%s" % consumer_uuid
        # New allocations should only include the destination...
        expected_payload = {
            'allocations': {
                uuids.destination: {
                    'resource_provider_generation': 42,
                    'resources': {
                        'VCPU': 1,
                        'MEMORY_MB': 1024,
                    },
                },
            },
            'consumer_generation': 1,
            'project_id': project_id,
            'user_id': user_id
        }

        self.assertEqual(
            [
                mock.call(
                    '/allocations/%s' % consumer_uuid,
                    global_request_id=self.context.global_id,
                    microversion='1.28'
                ),
                mock.call(
                    '/resource_providers?in_tree=%s' % uuids.source_compute,
                    global_request_id=self.context.global_id,
                    microversion='1.14'
                )
            ],
            self.ks_adap_mock.get.mock_calls)

        # We have to pull the json body from the mock call_args to validate
        # it separately otherwise hash seed issues get in the way.
        actual_payload = self.ks_adap_mock.put.call_args[1]['json']
        self.assertEqual(expected_payload, actual_payload)
        self.ks_adap_mock.put.assert_called_once_with(
            expected_url, microversion='1.28', json=mock.ANY,
            global_request_id=self.context.global_id)

        self.assertTrue(res)


class TestMoveAllocations(SchedulerReportClientTestCase):

    def setUp(self):
        super(TestMoveAllocations, self).setUp()
        # We want to reuse the mock throughout the class, but with
        # different return values.
        patcher = mock.patch(
            'nova.scheduler.client.report.SchedulerReportClient.post')
        self.mock_post = patcher.start()
        self.addCleanup(patcher.stop)
        self.mock_post.return_value.status_code = 204
        self.rp_uuid = mock.sentinel.rp
        self.consumer_uuid = mock.sentinel.consumer
        self.data = {"MEMORY_MB": 1024}
        patcher = mock.patch(
            'nova.scheduler.client.report.SchedulerReportClient.get')
        self.mock_get = patcher.start()
        self.addCleanup(patcher.stop)

        self.project_id = mock.sentinel.project_id
        self.user_id = mock.sentinel.user_id

        self.mock_post.return_value.status_code = 204
        self.rp_uuid = mock.sentinel.rp
        self.source_consumer_uuid = mock.sentinel.source_consumer
        self.target_consumer_uuid = mock.sentinel.target_consumer
        self.source_consumer_data = {
            "allocations": {
                self.rp_uuid: {
                    "generation": 1,
                    "resources": {
                        "MEMORY_MB": 1024
                    }
                }
            },
            "consumer_generation": 2,
            "project_id": self.project_id,
            "user_id": self.user_id
        }
        self.source_rsp = mock.Mock()
        self.source_rsp.json.return_value = self.source_consumer_data
        self.target_consumer_data = {
            "allocations": {
                self.rp_uuid: {
                    "generation": 1,
                    "resources": {
                        "MEMORY_MB": 2048
                    }
                }
            },
            "consumer_generation": 1,
            "project_id": self.project_id,
            "user_id": self.user_id
        }
        self.target_rsp = mock.Mock()
        self.target_rsp.json.return_value = self.target_consumer_data
        self.mock_get.side_effect = [self.source_rsp, self.target_rsp]
        self.expected_url = '/allocations'
        self.expected_microversion = '1.28'

    def test_url_microversion(self):
        resp = self.client.move_allocations(
            self.context, self.source_consumer_uuid, self.target_consumer_uuid)

        self.assertTrue(resp)
        self.mock_post.assert_called_once_with(
            self.expected_url, mock.ANY,
            version=self.expected_microversion,
            global_request_id=self.context.global_id)

    def test_move_to_empty_target(self):
        self.target_consumer_data = {"allocations": {}}
        target_rsp = mock.Mock()
        target_rsp.json.return_value = self.target_consumer_data
        self.mock_get.side_effect = [self.source_rsp, target_rsp]

        expected_payload = {
            self.target_consumer_uuid: {
                "allocations": {
                    self.rp_uuid: {
                        "resources": {
                            "MEMORY_MB": 1024
                        },
                        "generation": 1
                    }
                },
                "consumer_generation": None,
                "project_id": self.project_id,
                "user_id": self.user_id,
            },
            self.source_consumer_uuid: {
                "allocations": {},
                "consumer_generation": 2,
                "project_id": self.project_id,
                "user_id": self.user_id,
            }
        }

        resp = self.client.move_allocations(
            self.context, self.source_consumer_uuid, self.target_consumer_uuid)

        self.assertTrue(resp)
        self.mock_post.assert_called_once_with(
            self.expected_url, expected_payload,
            version=self.expected_microversion,
            global_request_id=self.context.global_id)

    @mock.patch('nova.scheduler.client.report.LOG.info')
    def test_move_from_empty_source(self, mock_info):
        """Tests the case that the target has allocations but the source does
        not so the move_allocations method assumes the allocations were already
        moved and returns True without trying to POST /allocations.
        """
        source_consumer_data = {"allocations": {}}
        source_rsp = mock.Mock()
        source_rsp.json.return_value = source_consumer_data
        self.mock_get.side_effect = [source_rsp, self.target_rsp]

        resp = self.client.move_allocations(
            self.context, self.source_consumer_uuid, self.target_consumer_uuid)

        self.assertTrue(resp)
        self.mock_post.assert_not_called()
        mock_info.assert_called_once()
        self.assertIn('Allocations not found for consumer',
                      mock_info.call_args[0][0])

    def test_move_to_non_empty_target(self):
        self.mock_get.side_effect = [self.source_rsp, self.target_rsp]

        expected_payload = {
            self.target_consumer_uuid: {
                "allocations": {
                    self.rp_uuid: {
                        "resources": {
                            "MEMORY_MB": 1024
                        },
                        "generation": 1
                    }
                },
                "consumer_generation": 1,
                "project_id": self.project_id,
                "user_id": self.user_id,
            },
            self.source_consumer_uuid: {
                "allocations": {},
                "consumer_generation": 2,
                "project_id": self.project_id,
                "user_id": self.user_id,
            }
        }

        with fixtures.EnvironmentVariable('OS_DEBUG', '1'):
            with nova_fixtures.StandardLogging() as stdlog:
                resp = self.client.move_allocations(
                    self.context, self.source_consumer_uuid,
                    self.target_consumer_uuid)

        self.assertTrue(resp)
        self.mock_post.assert_called_once_with(
            self.expected_url, expected_payload,
            version=self.expected_microversion,
            global_request_id=self.context.global_id)
        self.assertIn('Overwriting current allocation',
                      stdlog.logger.output)

    @mock.patch('time.sleep')
    def test_409_concurrent_provider_update(self, mock_sleep):
        # there will be 1 normal call and 3 retries
        self.mock_get.side_effect = [self.source_rsp, self.target_rsp,
                                     self.source_rsp, self.target_rsp,
                                     self.source_rsp, self.target_rsp,
                                     self.source_rsp, self.target_rsp]
        rsp = fake_requests.FakeResponse(
            409,
            jsonutils.dumps(
                {'errors': [
                    {'code': 'placement.concurrent_update',
                     'detail': ''}]}))

        self.mock_post.return_value = rsp

        resp = self.client.move_allocations(
            self.context, self.source_consumer_uuid, self.target_consumer_uuid)

        self.assertFalse(resp)
        # Post was attempted four times.
        self.assertEqual(4, self.mock_post.call_count)

    @mock.patch('nova.scheduler.client.report.LOG.warning')
    def test_not_409_failure(self, mock_log):
        error_message = 'placement not there'
        self.mock_post.return_value.status_code = 503
        self.mock_post.return_value.text = error_message

        resp = self.client.move_allocations(
            self.context, self.source_consumer_uuid, self.target_consumer_uuid)

        self.assertFalse(resp)
        args, kwargs = mock_log.call_args
        log_message = args[0]
        log_args = args[1]
        self.assertIn('Unable to post allocations', log_message)
        self.assertEqual(error_message, log_args['text'])

    def test_409_concurrent_consumer_update(self):
        self.mock_post.return_value = fake_requests.FakeResponse(
            status_code=409,
            content=jsonutils.dumps(
                {'errors': [{'code': 'placement.concurrent_update',
                             'detail': 'consumer generation conflict'}]}))

        self.assertRaises(exception.AllocationMoveFailed,
            self.client.move_allocations, self.context,
            self.source_consumer_uuid, self.target_consumer_uuid)


class TestProviderOperations(SchedulerReportClientTestCase):
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_create_resource_provider')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_get_inventory')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_get_provider_aggregates')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_provider_traits')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_get_sharing_providers')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_providers_in_tree')
    def test_ensure_resource_provider_get(self, get_rpt_mock, get_shr_mock,
            get_trait_mock, get_agg_mock, get_inv_mock, create_rp_mock):
        # No resource provider exists in the client's cache, so validate that
        # if we get the resource provider from the placement API that we don't
        # try to create the resource provider.
        get_rpt_mock.return_value = [{
            'uuid': uuids.compute_node,
            'name': mock.sentinel.name,
            'generation': 1,
        }]

        get_inv_mock.return_value = None
        get_agg_mock.return_value = report.AggInfo(
            aggregates=set([uuids.agg1]), generation=42)
        get_trait_mock.return_value = report.TraitInfo(
            traits=set(['CUSTOM_GOLD']), generation=43)
        get_shr_mock.return_value = []

        def assert_cache_contents():
            self.assertTrue(
                self.client._provider_tree.exists(uuids.compute_node))
            self.assertTrue(
                self.client._provider_tree.in_aggregates(uuids.compute_node,
                                                         [uuids.agg1]))
            self.assertFalse(
                self.client._provider_tree.in_aggregates(uuids.compute_node,
                                                         [uuids.agg2]))
            self.assertTrue(
                self.client._provider_tree.has_traits(uuids.compute_node,
                                                      ['CUSTOM_GOLD']))
            self.assertFalse(
                self.client._provider_tree.has_traits(uuids.compute_node,
                                                      ['CUSTOM_SILVER']))
            data = self.client._provider_tree.data(uuids.compute_node)
            self.assertEqual(43, data.generation)

        self.client._ensure_resource_provider(self.context, uuids.compute_node)

        assert_cache_contents()
        get_rpt_mock.assert_called_once_with(self.context, uuids.compute_node)
        get_agg_mock.assert_called_once_with(self.context, uuids.compute_node)
        get_trait_mock.assert_called_once_with(self.context,
                                               uuids.compute_node)
        get_shr_mock.assert_called_once_with(self.context, set([uuids.agg1]))
        self.assertFalse(create_rp_mock.called)

        # Now that the cache is populated, a subsequent call should be a no-op.
        get_rpt_mock.reset_mock()
        get_agg_mock.reset_mock()
        get_trait_mock.reset_mock()
        get_shr_mock.reset_mock()

        self.client._ensure_resource_provider(self.context, uuids.compute_node)

        assert_cache_contents()
        get_rpt_mock.assert_not_called()
        get_agg_mock.assert_not_called()
        get_trait_mock.assert_not_called()
        get_shr_mock.assert_not_called()
        create_rp_mock.assert_not_called()

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_create_resource_provider')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_refresh_associations')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_providers_in_tree')
    def test_ensure_resource_provider_create_fail(self, get_rpt_mock,
            refresh_mock, create_rp_mock):
        # No resource provider exists in the client's cache, and
        # _create_provider raises, indicating there was an error with the
        # create call. Ensure we don't populate the resource provider cache
        get_rpt_mock.return_value = []
        create_rp_mock.side_effect = exception.ResourceProviderCreationFailed(
            name=uuids.compute_node)

        self.assertRaises(
            exception.ResourceProviderCreationFailed,
            self.client._ensure_resource_provider, self.context,
            uuids.compute_node)

        get_rpt_mock.assert_called_once_with(self.context, uuids.compute_node)
        create_rp_mock.assert_called_once_with(
            self.context, uuids.compute_node, uuids.compute_node,
            parent_provider_uuid=None)
        self.assertFalse(self.client._provider_tree.exists(uuids.compute_node))
        self.assertFalse(refresh_mock.called)
        self.assertRaises(
            ValueError,
            self.client._provider_tree.in_aggregates, uuids.compute_node, [])
        self.assertRaises(
            ValueError,
            self.client._provider_tree.has_traits, uuids.compute_node, [])

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_create_resource_provider', return_value=None)
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_refresh_associations')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_providers_in_tree')
    def test_ensure_resource_provider_create_no_placement(self, get_rpt_mock,
            refresh_mock, create_rp_mock):
        # No resource provider exists in the client's cache, and
        # @safe_connect on _create_resource_provider returns None because
        # Placement isn't running yet. Ensure we don't populate the resource
        # provider cache.
        get_rpt_mock.return_value = []

        self.assertRaises(
            exception.ResourceProviderCreationFailed,
            self.client._ensure_resource_provider, self.context,
            uuids.compute_node)

        get_rpt_mock.assert_called_once_with(self.context, uuids.compute_node)
        create_rp_mock.assert_called_once_with(
            self.context, uuids.compute_node, uuids.compute_node,
            parent_provider_uuid=None)
        self.assertFalse(self.client._provider_tree.exists(uuids.compute_node))
        refresh_mock.assert_not_called()
        self.assertRaises(
            ValueError,
            self.client._provider_tree.in_aggregates, uuids.compute_node, [])
        self.assertRaises(
            ValueError,
            self.client._provider_tree.has_traits, uuids.compute_node, [])

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_create_resource_provider')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_refresh_and_get_inventory')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_refresh_associations')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_providers_in_tree')
    def test_ensure_resource_provider_create(self, get_rpt_mock,
                                             refresh_inv_mock,
                                             refresh_assoc_mock,
                                             create_rp_mock):
        # No resource provider exists in the client's cache and no resource
        # provider was returned from the placement API, so verify that in this
        # case we try to create the resource provider via the placement API.
        get_rpt_mock.return_value = []
        create_rp_mock.return_value = {
            'uuid': uuids.compute_node,
            'name': 'compute-name',
            'generation': 1,
        }
        self.assertEqual(
            uuids.compute_node,
            self.client._ensure_resource_provider(self.context,
                                                  uuids.compute_node))
        self._validate_provider(uuids.compute_node, name='compute-name',
                                generation=1, parent_uuid=None,
                                aggregates=set(), traits=set())

        # We don't refresh for a just-created provider
        refresh_inv_mock.assert_not_called()
        refresh_assoc_mock.assert_not_called()
        get_rpt_mock.assert_called_once_with(self.context, uuids.compute_node)
        create_rp_mock.assert_called_once_with(
                self.context,
                uuids.compute_node,
                uuids.compute_node,  # name param defaults to UUID if None
                parent_provider_uuid=None,
        )
        self.assertTrue(self.client._provider_tree.exists(uuids.compute_node))

        create_rp_mock.reset_mock()

        # Validate the path where we specify a name (don't default to the UUID)
        self.client._ensure_resource_provider(
            self.context, uuids.cn2, 'a-name')
        create_rp_mock.assert_called_once_with(
                self.context, uuids.cn2, 'a-name', parent_provider_uuid=None)

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_refresh_associations')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_create_resource_provider')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_providers_in_tree')
    def test_ensure_resource_provider_tree(self, get_rpt_mock, create_rp_mock,
                                           refresh_mock):
        """Test _ensure_resource_provider with a tree of providers."""
        def _create_resource_provider(context, uuid, name,
                                      parent_provider_uuid=None):
            """Mock side effect for creating the RP with the specified args."""
            return {
                'uuid': uuid,
                'name': name,
                'generation': 0,
                'parent_provider_uuid': parent_provider_uuid
            }
        create_rp_mock.side_effect = _create_resource_provider

        # We at least have to simulate the part of _refresh_associations that
        # marks a provider as 'seen'
        def mocked_refresh(context, rp_uuid, **kwargs):
            self.client._association_refresh_time[rp_uuid] = time.time()
        refresh_mock.side_effect = mocked_refresh

        # Not initially in the placement database, so we have to create it.
        get_rpt_mock.return_value = []

        # Create the root
        root = self.client._ensure_resource_provider(self.context, uuids.root)
        self.assertEqual(uuids.root, root)

        # Now create a child
        child1 = self.client._ensure_resource_provider(
            self.context, uuids.child1, name='junior',
            parent_provider_uuid=uuids.root)
        self.assertEqual(uuids.child1, child1)

        # If we re-ensure the child, we get the object from the tree, not a
        # newly-created one - i.e. the early .find() works like it should.
        self.assertIs(child1,
                      self.client._ensure_resource_provider(self.context,
                                                            uuids.child1))

        # Make sure we can create a grandchild
        grandchild = self.client._ensure_resource_provider(
            self.context, uuids.grandchild,
            parent_provider_uuid=uuids.child1)
        self.assertEqual(uuids.grandchild, grandchild)

        # Now create a second child of the root and make sure it doesn't wind
        # up in some crazy wrong place like under child1 or grandchild
        child2 = self.client._ensure_resource_provider(
            self.context, uuids.child2, parent_provider_uuid=uuids.root)
        self.assertEqual(uuids.child2, child2)

        all_rp_uuids = [uuids.root, uuids.child1, uuids.child2,
                        uuids.grandchild]

        # At this point we should get all the providers.
        self.assertEqual(
            set(all_rp_uuids),
            set(self.client._provider_tree.get_provider_uuids()))

        # And now _ensure is a no-op because everything is cached
        get_rpt_mock.reset_mock()
        create_rp_mock.reset_mock()
        refresh_mock.reset_mock()

        for rp_uuid in all_rp_uuids:
            self.client._ensure_resource_provider(self.context, rp_uuid)
        get_rpt_mock.assert_not_called()
        create_rp_mock.assert_not_called()
        refresh_mock.assert_not_called()

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_providers_in_tree')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_refresh_associations')
    def test_ensure_resource_provider_refresh_fetch(self, mock_ref_assoc,
                                                    mock_gpit):
        """Make sure refreshes are called with the appropriate UUIDs and flags
        when we fetch the provider tree from placement.
        """
        tree_uuids = set([uuids.root, uuids.one, uuids.two])
        mock_gpit.return_value = [{'uuid': u, 'name': u, 'generation': 42}
                                  for u in tree_uuids]
        self.assertEqual(uuids.root,
                         self.client._ensure_resource_provider(self.context,
                                                               uuids.root))
        mock_gpit.assert_called_once_with(self.context, uuids.root)
        mock_ref_assoc.assert_has_calls(
            [mock.call(self.context, uuid, force=True)
             for uuid in tree_uuids])
        self.assertEqual(tree_uuids,
                         set(self.client._provider_tree.get_provider_uuids()))

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_providers_in_tree')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_create_resource_provider')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_refresh_associations')
    def test_ensure_resource_provider_refresh_create(self, mock_refresh,
            mock_create, mock_gpit):
        """Make sure refresh is not called when we create the RP."""
        mock_gpit.return_value = []
        mock_create.return_value = {'name': 'cn', 'uuid': uuids.cn,
                                    'generation': 42}
        self.assertEqual(uuids.root,
                         self.client._ensure_resource_provider(self.context,
                                                               uuids.root))
        mock_gpit.assert_called_once_with(self.context, uuids.root)
        mock_create.assert_called_once_with(self.context, uuids.root,
                                            uuids.root,
                                            parent_provider_uuid=None)
        mock_refresh.assert_not_called()
        self.assertEqual([uuids.cn],
                         self.client._provider_tree.get_provider_uuids())

    def test_get_allocation_candidates(self):
        resp_mock = mock.Mock(status_code=200)
        json_data = {
            'allocation_requests': mock.sentinel.alloc_reqs,
            'provider_summaries': mock.sentinel.p_sums,
        }
        flavor = objects.Flavor(
            vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0,
            extra_specs={
                'resources:VCPU': '1',
                'resources:MEMORY_MB': '1024',
                'trait:HW_CPU_X86_AVX': 'required',
                'trait:CUSTOM_TRAIT1': 'required',
                'trait:CUSTOM_TRAIT2': 'preferred',
                'trait:CUSTOM_TRAIT3': 'forbidden',
                'trait:CUSTOM_TRAIT4': 'forbidden',
                'resources_DISK:DISK_GB': '30',
                'trait_DISK:STORAGE_DISK_SSD': 'required',
                'resources2:VGPU': '2',
                'trait2:HW_GPU_RESOLUTION_W2560H1600': 'required',
                'trait2:HW_GPU_API_VULKAN': 'required',
                'resources_NET:SRIOV_NET_VF': '1',
                'resources_NET:CUSTOM_NET_EGRESS_BYTES_SEC': '125000',
                'group_policy': 'isolate',
                # These are ignored because misspelled, bad value, etc.
                'resources*2:CUSTOM_WIDGET': '123',
                'trait:HW_NIC_OFFLOAD_LRO': 'preferred',
                'group_policy3': 'none',
            })
        req_spec = objects.RequestSpec(flavor=flavor, is_bfv=False)
        resources = scheduler_utils.ResourceRequest.from_request_spec(req_spec)
        resources.get_request_group(None).aggregates = [
            ['agg1', 'agg2', 'agg3'], ['agg1', 'agg2']]
        forbidden_aggs = set(['agg1', 'agg5', 'agg6'])
        resources.get_request_group(None).forbidden_aggregates = forbidden_aggs
        expected_path = '/allocation_candidates'
        expected_query = [
            ('group_policy', 'isolate'),
            ('limit', '1000'),
            ('member_of', '!in:agg1,agg5,agg6'),
            ('member_of', 'in:agg1,agg2'),
            ('member_of', 'in:agg1,agg2,agg3'),
            ('required', 'CUSTOM_TRAIT1,HW_CPU_X86_AVX,!CUSTOM_TRAIT3,'
                         '!CUSTOM_TRAIT4'),
            ('required2', 'HW_GPU_API_VULKAN,HW_GPU_RESOLUTION_W2560H1600'),
            ('required_DISK', 'STORAGE_DISK_SSD'),
            ('resources', 'MEMORY_MB:1024,VCPU:1'),
            ('resources2', 'VGPU:2'),
            ('resources_DISK', 'DISK_GB:30'),
            ('resources_NET',
             'CUSTOM_NET_EGRESS_BYTES_SEC:125000,SRIOV_NET_VF:1')
        ]

        resp_mock.json.return_value = json_data
        self.ks_adap_mock.get.return_value = resp_mock

        alloc_reqs, p_sums, allocation_request_version = (
            self.client.get_allocation_candidates(self.context, resources))

        url = self.ks_adap_mock.get.call_args[0][0]
        split_url = parse.urlsplit(url)
        query = parse.parse_qsl(split_url.query)
        self.assertEqual(expected_path, split_url.path)
        self.assertEqual(expected_query, query)
        expected_url = '/allocation_candidates?%s' % parse.urlencode(
            expected_query)
        self.ks_adap_mock.get.assert_called_once_with(
            expected_url, microversion='1.36',
            global_request_id=self.context.global_id)
        self.assertEqual(mock.sentinel.alloc_reqs, alloc_reqs)
        self.assertEqual(mock.sentinel.p_sums, p_sums)

    def test_get_ac_no_trait_bogus_group_policy_custom_limit(self):
        self.flags(max_placement_results=42, group='scheduler')
        resp_mock = mock.Mock(status_code=200)
        json_data = {
            'allocation_requests': mock.sentinel.alloc_reqs,
            'provider_summaries': mock.sentinel.p_sums,
        }
        flavor = objects.Flavor(
            vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0,
            extra_specs={
                'resources:VCPU': '1',
                'resources:MEMORY_MB': '1024',
                'resources1:DISK_GB': '30',
                'group_policy': 'bogus',
            })
        req_spec = objects.RequestSpec(flavor=flavor, is_bfv=False)
        resources = scheduler_utils.ResourceRequest.from_request_spec(req_spec)
        expected_path = '/allocation_candidates'
        expected_query = [
            ('limit', '42'),
            ('resources', 'MEMORY_MB:1024,VCPU:1'),
            ('resources1', 'DISK_GB:30'),
        ]

        resp_mock.json.return_value = json_data
        self.ks_adap_mock.get.return_value = resp_mock

        alloc_reqs, p_sums, allocation_request_version = (
            self.client.get_allocation_candidates(self.context, resources))

        url = self.ks_adap_mock.get.call_args[0][0]
        split_url = parse.urlsplit(url)
        query = parse.parse_qsl(split_url.query)
        self.assertEqual(expected_path, split_url.path)
        self.assertEqual(expected_query, query)
        expected_url = '/allocation_candidates?%s' % parse.urlencode(
            expected_query)
        self.assertEqual(mock.sentinel.alloc_reqs, alloc_reqs)
        self.ks_adap_mock.get.assert_called_once_with(
            expected_url, microversion='1.36',
            global_request_id=self.context.global_id)
        self.assertEqual(mock.sentinel.p_sums, p_sums)

    def test_get_allocation_candidates_not_found(self):
        # Ensure _get_resource_provider() just returns None when the placement
        # API doesn't find a resource provider matching a UUID
        resp_mock = mock.Mock(status_code=404)
        self.ks_adap_mock.get.return_value = resp_mock
        expected_path = '/allocation_candidates'
        expected_query = {
            'resources': ['DISK_GB:15,MEMORY_MB:1024,VCPU:1'],
            'limit': ['100']
        }

        # Make sure we're also honoring the configured limit
        self.flags(max_placement_results=100, group='scheduler')

        flavor = objects.Flavor(
            vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0)
        req_spec = objects.RequestSpec(flavor=flavor, is_bfv=False)
        resources = scheduler_utils.ResourceRequest.from_request_spec(req_spec)

        res = self.client.get_allocation_candidates(self.context, resources)

        self.ks_adap_mock.get.assert_called_once_with(
            mock.ANY, microversion='1.36',
            global_request_id=self.context.global_id)
        url = self.ks_adap_mock.get.call_args[0][0]
        split_url = parse.urlsplit(url)
        query = parse.parse_qs(split_url.query)
        self.assertEqual(expected_path, split_url.path)
        self.assertEqual(expected_query, query)
        self.assertIsNone(res[0])

    def test_get_resource_provider_found(self):
        # Ensure _get_resource_provider() returns a dict of resource provider
        # if it finds a resource provider record from the placement API
        uuid = uuids.compute_node
        resp_mock = mock.Mock(status_code=200)
        json_data = {
            'uuid': uuid,
            'name': uuid,
            'generation': 42,
            'parent_provider_uuid': None,
        }
        resp_mock.json.return_value = json_data
        self.ks_adap_mock.get.return_value = resp_mock

        result = self.client._get_resource_provider(self.context, uuid)

        expected_provider_dict = dict(
                uuid=uuid,
                name=uuid,
                generation=42,
                parent_provider_uuid=None,
        )
        expected_url = '/resource_providers/' + uuid
        self.ks_adap_mock.get.assert_called_once_with(
            expected_url, microversion='1.14',
            global_request_id=self.context.global_id)
        self.assertEqual(expected_provider_dict, result)

    def test_get_resource_provider_not_found(self):
        # Ensure _get_resource_provider() just returns None when the placement
        # API doesn't find a resource provider matching a UUID
        resp_mock = mock.Mock(status_code=404)
        self.ks_adap_mock.get.return_value = resp_mock

        uuid = uuids.compute_node
        result = self.client._get_resource_provider(self.context, uuid)

        expected_url = '/resource_providers/' + uuid
        self.ks_adap_mock.get.assert_called_once_with(
            expected_url, microversion='1.14',
            global_request_id=self.context.global_id)
        self.assertIsNone(result)

    @mock.patch.object(report.LOG, 'error')
    def test_get_resource_provider_error(self, logging_mock):
        # Ensure _get_resource_provider() sets the error flag when trying to
        # communicate with the placement API and not getting an error we can
        # deal with
        resp_mock = mock.Mock(status_code=503)
        self.ks_adap_mock.get.return_value = resp_mock
        self.ks_adap_mock.get.return_value.headers = {
            'x-openstack-request-id': uuids.request_id}

        uuid = uuids.compute_node
        self.assertRaises(
            exception.ResourceProviderRetrievalFailed,
            self.client._get_resource_provider, self.context, uuid)

        expected_url = '/resource_providers/' + uuid
        self.ks_adap_mock.get.assert_called_once_with(
            expected_url, microversion='1.14',
            global_request_id=self.context.global_id)
        # A 503 Service Unavailable should trigger an error log that
        # includes the placement request id and return None
        # from _get_resource_provider()
        self.assertTrue(logging_mock.called)
        self.assertEqual(uuids.request_id,
                         logging_mock.call_args[0][1]['placement_req_id'])

    def test_get_sharing_providers(self):
        self.flags(
            sharing_providers_max_uuids_per_request=3, group='compute')

        resp_mock = mock.Mock(status_code=200)
        rpjson1 = [
            {
                'uuid': uuids.sharing1,
                'name': 'bandwidth_provider',
                'generation': 42,
                'parent_provider_uuid': None,
                'root_provider_uuid': None,
                'links': [],
            },
            {
                'uuid': uuids.sharing2,
                'name': 'storage_provider',
                'generation': 42,
                'parent_provider_uuid': None,
                'root_provider_uuid': None,
                'links': [],
            },
            {
                'uuid': uuids.sharing3,
                'name': 'storage_provider',
                'generation': 42,
                'parent_provider_uuid': None,
                'root_provider_uuid': None,
                'links': [],
            }
        ]
        rpjson2 = [
            {
                'uuid': uuids.sharing4,
                'name': 'storage_provider',
                'generation': 42,
                'parent_provider_uuid': None,
                'root_provider_uuid': None,
                'links': [],
            },
        ]
        resp_mock.json.side_effect = [
            {'resource_providers': rpjson1},
            {'resource_providers': rpjson2}
        ]
        self.ks_adap_mock.get.return_value = resp_mock
        result = self.client._get_sharing_providers(
            self.context, [uuids.agg1, uuids.agg2,
                           uuids.agg3, uuids.agg4])

        self.ks_adap_mock.get.assert_has_calls(
            [
                # Asserting first request with 3 uuids
                mock.call(
                    '/resource_providers?member_of=in:' +
                    ','.join((uuids.agg1, uuids.agg2, uuids.agg3)) +
                    '&required=MISC_SHARES_VIA_AGGREGATE',
                    microversion='1.18',
                    global_request_id=self.context.global_id),
                mock.call().json(),
                # Asserting second request with 1 uuid
                mock.call(
                    '/resource_providers?member_of=in:' +
                    uuids.agg4 +
                    '&required=MISC_SHARES_VIA_AGGREGATE',
                    microversion='1.18',
                    global_request_id=self.context.global_id),
                mock.call().json(),
            ])
        self.assertEqual(rpjson1 + rpjson2, result)

    def test_get_sharing_providers_emptylist(self):
        self.assertEqual(
            [], self.client._get_sharing_providers(self.context, []))
        self.ks_adap_mock.get.assert_not_called()

    @mock.patch.object(report.LOG, 'error')
    def test_get_sharing_providers_error(self, logging_mock):
        # Ensure _get_sharing_providers() logs an error and raises if the
        # placement API call doesn't respond 200
        resp_mock = mock.Mock(status_code=503)
        self.ks_adap_mock.get.return_value = resp_mock
        self.ks_adap_mock.get.return_value.headers = {
            'x-openstack-request-id': uuids.request_id}

        uuid = uuids.agg
        self.assertRaises(exception.ResourceProviderRetrievalFailed,
                          self.client._get_sharing_providers,
                          self.context, [uuid])

        expected_url = ('/resource_providers?member_of=in:' + uuid +
                        '&required=MISC_SHARES_VIA_AGGREGATE')
        self.ks_adap_mock.get.assert_called_once_with(
            expected_url, microversion='1.18',
            global_request_id=self.context.global_id)
        # A 503 Service Unavailable should trigger an error log that
        # includes the placement request id
        self.assertTrue(logging_mock.called)
        self.assertEqual(uuids.request_id,
                         logging_mock.call_args[0][1]['placement_req_id'])

    def test_get_providers_in_tree(self):
        # Ensure get_providers_in_tree() returns a list of resource
        # provider dicts if it finds a resource provider record from the
        # placement API
        root = uuids.compute_node
        child = uuids.child
        resp_mock = mock.Mock(status_code=200)
        rpjson = [
            {
                'uuid': root,
                'name': 'daddy', 'generation': 42,
                'parent_provider_uuid': None,
            },
            {
                'uuid': child,
                'name': 'junior',
                'generation': 42,
                'parent_provider_uuid': root,
            },
        ]
        resp_mock.json.return_value = {'resource_providers': rpjson}
        self.ks_adap_mock.get.return_value = resp_mock

        result = self.client.get_providers_in_tree(self.context, root)

        expected_url = '/resource_providers?in_tree=' + root
        self.ks_adap_mock.get.assert_called_once_with(
            expected_url, microversion='1.14',
            global_request_id=self.context.global_id)
        self.assertEqual(rpjson, result)

    @mock.patch.object(report.LOG, 'error')
    def test_get_providers_in_tree_error(self, logging_mock):
        # Ensure get_providers_in_tree() logs an error and raises if the
        # placement API call doesn't respond 200
        resp_mock = mock.Mock(status_code=503)
        self.ks_adap_mock.get.return_value = resp_mock
        self.ks_adap_mock.get.return_value.headers = {
            'x-openstack-request-id': 'req-' + uuids.request_id}

        uuid = uuids.compute_node
        self.assertRaises(exception.ResourceProviderRetrievalFailed,
                          self.client.get_providers_in_tree, self.context,
                          uuid)

        expected_url = '/resource_providers?in_tree=' + uuid
        self.ks_adap_mock.get.assert_called_once_with(
            expected_url, microversion='1.14',
            global_request_id=self.context.global_id)
        # A 503 Service Unavailable should trigger an error log that includes
        # the placement request id
        self.assertTrue(logging_mock.called)
        self.assertEqual('req-' + uuids.request_id,
                         logging_mock.call_args[0][1]['placement_req_id'])

    def test_get_providers_in_tree_ksa_exc(self):
        self.ks_adap_mock.get.side_effect = ks_exc.EndpointNotFound()
        self.assertRaises(
            ks_exc.ClientException,
            self.client.get_providers_in_tree, self.context, uuids.whatever)

    def test_create_resource_provider(self):
        """Test that _create_resource_provider() sends a dict of resource
        provider information without a parent provider UUID.
        """
        uuid = uuids.compute_node
        name = 'computehost'
        resp_mock = mock.Mock(status_code=200)
        self.ks_adap_mock.post.return_value = resp_mock

        self.assertEqual(
            resp_mock.json.return_value,
            self.client._create_resource_provider(self.context, uuid, name))

        expected_payload = {
            'uuid': uuid,
            'name': name,
        }

        expected_url = '/resource_providers'
        self.ks_adap_mock.post.assert_called_once_with(
            expected_url, json=expected_payload, microversion='1.20',
            global_request_id=self.context.global_id)

    def test_create_resource_provider_with_parent(self):
        """Test that when specifying a parent provider UUID, that the
        parent_provider_uuid part of the payload is properly specified.
        """
        parent_uuid = uuids.parent
        uuid = uuids.compute_node
        name = 'computehost'
        resp_mock = mock.Mock(status_code=200)
        self.ks_adap_mock.post.return_value = resp_mock

        self.assertEqual(
            resp_mock.json.return_value,
            self.client._create_resource_provider(
                self.context,
                uuid,
                name,
                parent_provider_uuid=parent_uuid,
            )
        )

        expected_payload = {
            'uuid': uuid,
            'name': name,
            'parent_provider_uuid': parent_uuid,
        }
        expected_url = '/resource_providers'
        self.ks_adap_mock.post.assert_called_once_with(
            expected_url, json=expected_payload, microversion='1.20',
            global_request_id=self.context.global_id)

    @mock.patch.object(report.LOG, 'info')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_get_resource_provider')
    def test_create_resource_provider_concurrent_create(self, get_rp_mock,
                                                        logging_mock):
        # Ensure _create_resource_provider() returns a dict of resource
        # provider gotten from _get_resource_provider() if the call to create
        # the resource provider in the placement API returned a 409 Conflict,
        # indicating another thread concurrently created the resource provider
        # record.
        uuid = uuids.compute_node
        name = 'computehost'
        self.ks_adap_mock.post.return_value = fake_requests.FakeResponse(
            409, content='not a name conflict',
            headers={'x-openstack-request-id': uuids.request_id})

        get_rp_mock.return_value = mock.sentinel.get_rp

        result = self.client._create_resource_provider(self.context, uuid,
                                                       name)

        expected_payload = {
            'uuid': uuid,
            'name': name,
        }
        expected_url = '/resource_providers'
        self.ks_adap_mock.post.assert_called_once_with(
            expected_url, json=expected_payload, microversion='1.20',
            global_request_id=self.context.global_id)
        self.assertEqual(mock.sentinel.get_rp, result)
        # The 409 response will produce a message to the info log.
        self.assertTrue(logging_mock.called)
        self.assertEqual(uuids.request_id,
                        logging_mock.call_args[0][1]['placement_req_id'])

    def test_create_resource_provider_name_conflict(self):
        # When the API call to create the resource provider fails 409 with a
        # name conflict, we raise an exception.
        self.ks_adap_mock.post.return_value = fake_requests.FakeResponse(
            409, content='<stuff>Conflicting resource provider name: foo '
                         'already exists.</stuff>')

        self.assertRaises(
            exception.ResourceProviderCreationFailed,
            self.client._create_resource_provider, self.context,
            uuids.compute_node, 'foo')

    @mock.patch.object(report.LOG, 'error')
    def test_create_resource_provider_error(self, logging_mock):
        # Ensure _create_resource_provider() sets the error flag when trying to
        # communicate with the placement API and not getting an error we can
        # deal with
        uuid = uuids.compute_node
        name = 'computehost'
        self.ks_adap_mock.post.return_value = fake_requests.FakeResponse(
            503, headers={'x-openstack-request-id': uuids.request_id})

        self.assertRaises(
            exception.ResourceProviderCreationFailed,
            self.client._create_resource_provider, self.context, uuid, name)

        expected_payload = {
            'uuid': uuid,
            'name': name,
        }
        expected_url = '/resource_providers'
        self.ks_adap_mock.post.assert_called_once_with(
            expected_url, json=expected_payload, microversion='1.20',
            global_request_id=self.context.global_id)
        # A 503 Service Unavailable should log an error that
        # includes the placement request id and
        # _create_resource_provider() should return None
        self.assertTrue(logging_mock.called)
        self.assertEqual(uuids.request_id,
                        logging_mock.call_args[0][1]['placement_req_id'])

    def test_put_empty(self):
        # A simple put with an empty (not None) payload should send the empty
        # payload through.
        # Bug #1744786
        url = '/resource_providers/%s/aggregates' % uuids.foo
        self.client.put(url, [])
        self.ks_adap_mock.put.assert_called_once_with(
            url, json=[], microversion=None, global_request_id=None)

    def test_delete_provider(self):
        delete_mock = fake_requests.FakeResponse(None)
        self.ks_adap_mock.delete.return_value = delete_mock

        for status_code in (204, 404):
            delete_mock.status_code = status_code
            # Seed the caches
            self.client._provider_tree.new_root('compute', uuids.root,
                                                generation=0)
            self.client._association_refresh_time[uuids.root] = 1234

            self.client._delete_provider(uuids.root, global_request_id='gri')

            self.ks_adap_mock.delete.assert_called_once_with(
                '/resource_providers/' + uuids.root,
                global_request_id='gri', microversion=None)
            self.assertFalse(self.client._provider_tree.exists(uuids.root))
            self.assertNotIn(uuids.root, self.client._association_refresh_time)

            self.ks_adap_mock.delete.reset_mock()

    def test_delete_provider_fail(self):
        delete_mock = fake_requests.FakeResponse(None)
        self.ks_adap_mock.delete.return_value = delete_mock
        resp_exc_map = {409: exception.ResourceProviderInUse,
                        503: exception.ResourceProviderDeletionFailed}

        for status_code, exc in resp_exc_map.items():
            delete_mock.status_code = status_code
            self.assertRaises(exc, self.client._delete_provider, uuids.root)
            self.ks_adap_mock.delete.assert_called_once_with(
                '/resource_providers/' + uuids.root, microversion=None,
                global_request_id=None)

            self.ks_adap_mock.delete.reset_mock()

    def test_set_aggregates_for_provider(self):
        aggs = [uuids.agg1, uuids.agg2]
        self.ks_adap_mock.put.return_value = fake_requests.FakeResponse(
            200, content=jsonutils.dumps({
                'aggregates': aggs,
                'resource_provider_generation': 1}))

        # Prime the provider tree cache
        self.client._provider_tree.new_root('rp', uuids.rp, generation=0)
        self.assertEqual(set(),
                         self.client._provider_tree.data(uuids.rp).aggregates)

        self.client.set_aggregates_for_provider(self.context, uuids.rp, aggs)

        exp_payload = {'aggregates': aggs,
                       'resource_provider_generation': 0}
        self.ks_adap_mock.put.assert_called_once_with(
            '/resource_providers/%s/aggregates' % uuids.rp, json=exp_payload,
            microversion='1.19',
            global_request_id=self.context.global_id)
        # Cache was updated
        ptree_data = self.client._provider_tree.data(uuids.rp)
        self.assertEqual(set(aggs), ptree_data.aggregates)
        self.assertEqual(1, ptree_data.generation)

    def test_set_aggregates_for_provider_bad_args(self):
        self.assertRaises(ValueError, self.client.set_aggregates_for_provider,
                          self.context, uuids.rp, {}, use_cache=False)
        self.assertRaises(ValueError, self.client.set_aggregates_for_provider,
                          self.context, uuids.rp, {}, use_cache=False,
                          generation=None)

    def test_set_aggregates_for_provider_fail(self):
        self.ks_adap_mock.put.return_value = fake_requests.FakeResponse(503)
        # Prime the provider tree cache
        self.client._provider_tree.new_root('rp', uuids.rp, generation=0)
        self.assertRaises(
            exception.ResourceProviderUpdateFailed,
            self.client.set_aggregates_for_provider,
            self.context, uuids.rp, [uuids.agg])
        # The cache wasn't updated
        self.assertEqual(set(),
                         self.client._provider_tree.data(uuids.rp).aggregates)

    def test_set_aggregates_for_provider_conflict(self):
        # Prime the provider tree cache
        self.client._provider_tree.new_root('rp', uuids.rp, generation=0)
        self.ks_adap_mock.put.return_value = fake_requests.FakeResponse(409)
        self.assertRaises(
            exception.ResourceProviderUpdateConflict,
            self.client.set_aggregates_for_provider,
            self.context, uuids.rp, [uuids.agg])
        # The cache was invalidated
        self.assertNotIn(uuids.rp,
                         self.client._provider_tree.get_provider_uuids())
        self.assertNotIn(uuids.rp, self.client._association_refresh_time)

    def test_set_aggregates_for_provider_short_circuit(self):
        """No-op when aggregates have not changed."""
        # Prime the provider tree cache
        self.client._provider_tree.new_root('rp', uuids.rp, generation=7)
        self.client.set_aggregates_for_provider(self.context, uuids.rp, [])
        self.ks_adap_mock.put.assert_not_called()

    def test_set_aggregates_for_provider_no_short_circuit(self):
        """Don't short-circuit if generation doesn't match, even if aggs have
        not changed.
        """
        # Prime the provider tree cache
        self.client._provider_tree.new_root('rp', uuids.rp, generation=2)
        self.ks_adap_mock.put.return_value = fake_requests.FakeResponse(
            200, content=jsonutils.dumps({
                'aggregates': [],
                'resource_provider_generation': 5}))
        self.client.set_aggregates_for_provider(self.context, uuids.rp, [],
                                                generation=4)
        exp_payload = {'aggregates': [],
                       'resource_provider_generation': 4}
        self.ks_adap_mock.put.assert_called_once_with(
            '/resource_providers/%s/aggregates' % uuids.rp, json=exp_payload,
            microversion='1.19',
            global_request_id=self.context.global_id)
        # Cache was updated
        ptree_data = self.client._provider_tree.data(uuids.rp)
        self.assertEqual(set(), ptree_data.aggregates)
        self.assertEqual(5, ptree_data.generation)

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_get_resource_provider', return_value=mock.NonCallableMock)
    def test_get_resource_provider_name_from_cache(self, mock_placement_get):
        expected_name = 'rp'
        self.client._provider_tree.new_root(
            expected_name, uuids.rp, generation=0)

        actual_name = self.client.get_resource_provider_name(
            self.context, uuids.rp)

        self.assertEqual(expected_name, actual_name)

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_get_resource_provider')
    def test_get_resource_provider_name_from_placement(
            self, mock_placement_get):
        expected_name = 'rp'
        mock_placement_get.return_value = {
            'uuid': uuids.rp,
            'name': expected_name
        }

        actual_name = self.client.get_resource_provider_name(
            self.context, uuids.rp)

        self.assertEqual(expected_name, actual_name)
        mock_placement_get.assert_called_once_with(self.context, uuids.rp)

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_get_resource_provider')
    def test_get_resource_provider_name_rp_not_found_in_placement(
            self, mock_placement_get):
        mock_placement_get.side_effect = \
            exception.ResourceProviderNotFound(uuids.rp)

        self.assertRaises(
            exception.ResourceProviderNotFound,
            self.client.get_resource_provider_name,
            self.context, uuids.rp)

        mock_placement_get.assert_called_once_with(self.context, uuids.rp)

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_get_resource_provider')
    def test_get_resource_provider_name_placement_unavailable(
            self, mock_placement_get):
        mock_placement_get.side_effect = \
            exception.ResourceProviderRetrievalFailed(uuid=uuids.rp)

        self.assertRaises(
            exception.ResourceProviderRetrievalFailed,
            self.client.get_resource_provider_name,
            self.context, uuids.rp)


class TestAggregates(SchedulerReportClientTestCase):
    def test_get_provider_aggregates_found(self):
        uuid = uuids.compute_node
        resp_mock = mock.Mock(status_code=200)
        aggs = [
            uuids.agg1,
            uuids.agg2,
        ]
        resp_mock.json.return_value = {'aggregates': aggs,
                                       'resource_provider_generation': 42}
        self.ks_adap_mock.get.return_value = resp_mock

        result, gen = self.client._get_provider_aggregates(self.context, uuid)

        expected_url = '/resource_providers/' + uuid + '/aggregates'
        self.ks_adap_mock.get.assert_called_once_with(
            expected_url, microversion='1.19',
            global_request_id=self.context.global_id)
        self.assertEqual(set(aggs), result)
        self.assertEqual(42, gen)

    @mock.patch.object(report.LOG, 'error')
    def test_get_provider_aggregates_error(self, log_mock):
        """Test that when the placement API returns any error when looking up a
        provider's aggregates, we raise an exception.
        """
        uuid = uuids.compute_node
        resp_mock = mock.Mock(headers={
            'x-openstack-request-id': uuids.request_id})
        self.ks_adap_mock.get.return_value = resp_mock

        for status_code in (400, 404, 503):
            resp_mock.status_code = status_code
            self.assertRaises(
                exception.ResourceProviderAggregateRetrievalFailed,
                self.client._get_provider_aggregates, self.context, uuid)

            expected_url = '/resource_providers/' + uuid + '/aggregates'
            self.ks_adap_mock.get.assert_called_once_with(
                expected_url, microversion='1.19',
                global_request_id=self.context.global_id)
            self.assertTrue(log_mock.called)
            self.assertEqual(uuids.request_id,
                             log_mock.call_args[0][1]['placement_req_id'])
            self.ks_adap_mock.get.reset_mock()
            log_mock.reset_mock()


class TestTraits(SchedulerReportClientTestCase):
    trait_api_kwargs = {'microversion': '1.6'}

    def test_get_provider_traits_found(self):
        uuid = uuids.compute_node
        resp_mock = mock.Mock(status_code=200)
        traits = [
            'CUSTOM_GOLD',
            'CUSTOM_SILVER',
        ]
        resp_mock.json.return_value = {'traits': traits,
                                       'resource_provider_generation': 42}
        self.ks_adap_mock.get.return_value = resp_mock

        result, gen = self.client.get_provider_traits(self.context, uuid)

        expected_url = '/resource_providers/' + uuid + '/traits'
        self.ks_adap_mock.get.assert_called_once_with(
            expected_url,
            global_request_id=self.context.global_id,
            **self.trait_api_kwargs)
        self.assertEqual(set(traits), result)
        self.assertEqual(42, gen)

    @mock.patch.object(report.LOG, 'error')
    def test_get_provider_traits_error(self, log_mock):
        """Test that when the placement API returns any error when looking up a
        provider's traits, we raise an exception.
        """
        uuid = uuids.compute_node
        resp_mock = mock.Mock(headers={
            'x-openstack-request-id': uuids.request_id})
        self.ks_adap_mock.get.return_value = resp_mock

        for status_code in (400, 404, 503):
            resp_mock.status_code = status_code
            self.assertRaises(
                exception.ResourceProviderTraitRetrievalFailed,
                self.client.get_provider_traits, self.context, uuid)

            expected_url = '/resource_providers/' + uuid + '/traits'
            self.ks_adap_mock.get.assert_called_once_with(
                expected_url,
                global_request_id=self.context.global_id,
                **self.trait_api_kwargs)
            self.assertTrue(log_mock.called)
            self.assertEqual(uuids.request_id,
                             log_mock.call_args[0][1]['placement_req_id'])
            self.ks_adap_mock.get.reset_mock()
            log_mock.reset_mock()

    def test_get_provider_traits_placement_comm_error(self):
        """ksa ClientException raises through."""
        uuid = uuids.compute_node
        self.ks_adap_mock.get.side_effect = ks_exc.EndpointNotFound()
        self.assertRaises(ks_exc.ClientException,
                          self.client.get_provider_traits, self.context, uuid)
        expected_url = '/resource_providers/' + uuid + '/traits'
        self.ks_adap_mock.get.assert_called_once_with(
            expected_url,
            global_request_id=self.context.global_id,
            **self.trait_api_kwargs)

    def test_ensure_traits(self):
        """Successful paths, various permutations of traits existing or needing
        to be created.
        """
        standard_traits = ['HW_NIC_OFFLOAD_UCS', 'HW_NIC_OFFLOAD_RDMA']
        custom_traits = ['CUSTOM_GOLD', 'CUSTOM_SILVER']
        all_traits = standard_traits + custom_traits

        get_mock = mock.Mock(status_code=200)
        self.ks_adap_mock.get.return_value = get_mock

        # Request all traits; custom traits need to be created
        get_mock.json.return_value = {'traits': standard_traits}
        self.client._ensure_traits(self.context, all_traits)
        self.ks_adap_mock.get.assert_called_once_with(
            '/traits?name=in:' + ','.join(all_traits),
            global_request_id=self.context.global_id,
            **self.trait_api_kwargs)
        self.ks_adap_mock.put.assert_has_calls(
            [mock.call('/traits/' + trait,
             global_request_id=self.context.global_id, json=None,
             **self.trait_api_kwargs)
             for trait in custom_traits], any_order=True)

        self.ks_adap_mock.reset_mock()

        # Request standard traits; no traits need to be created
        get_mock.json.return_value = {'traits': standard_traits}
        self.client._ensure_traits(self.context, standard_traits)
        self.ks_adap_mock.get.assert_called_once_with(
            '/traits?name=in:' + ','.join(standard_traits),
            global_request_id=self.context.global_id,
            **self.trait_api_kwargs)
        self.ks_adap_mock.put.assert_not_called()

        self.ks_adap_mock.reset_mock()

        # Request no traits - short circuit
        self.client._ensure_traits(self.context, None)
        self.client._ensure_traits(self.context, [])
        self.ks_adap_mock.get.assert_not_called()
        self.ks_adap_mock.put.assert_not_called()

    def test_ensure_traits_fail_retrieval(self):
        self.ks_adap_mock.get.return_value = mock.Mock(status_code=400)

        self.assertRaises(exception.TraitRetrievalFailed,
                          self.client._ensure_traits,
                          self.context, ['FOO'])

        self.ks_adap_mock.get.assert_called_once_with(
            '/traits?name=in:FOO',
            global_request_id=self.context.global_id,
            **self.trait_api_kwargs)
        self.ks_adap_mock.put.assert_not_called()

    def test_ensure_traits_fail_creation(self):
        get_mock = mock.Mock(status_code=200)
        get_mock.json.return_value = {'traits': []}
        self.ks_adap_mock.get.return_value = get_mock
        self.ks_adap_mock.put.return_value = fake_requests.FakeResponse(400)

        self.assertRaises(exception.TraitCreationFailed,
                          self.client._ensure_traits,
                          self.context, ['FOO'])

        self.ks_adap_mock.get.assert_called_once_with(
            '/traits?name=in:FOO',
            global_request_id=self.context.global_id,
            **self.trait_api_kwargs)
        self.ks_adap_mock.put.assert_called_once_with(
            '/traits/FOO',
            global_request_id=self.context.global_id, json=None,
            **self.trait_api_kwargs)

    def test_set_traits_for_provider(self):
        traits = ['HW_NIC_OFFLOAD_UCS', 'HW_NIC_OFFLOAD_RDMA']

        # Make _ensure_traits succeed without PUTting
        get_mock = mock.Mock(status_code=200)
        get_mock.json.return_value = {'traits': traits}
        self.ks_adap_mock.get.return_value = get_mock

        # Prime the provider tree cache
        self.client._provider_tree.new_root('rp', uuids.rp, generation=0)

        # Mock the /rp/{u}/traits PUT to succeed
        put_mock = mock.Mock(status_code=200)
        put_mock.json.return_value = {'traits': traits,
                                      'resource_provider_generation': 1}
        self.ks_adap_mock.put.return_value = put_mock

        # Invoke
        self.client.set_traits_for_provider(self.context, uuids.rp, traits)

        # Verify API calls
        self.ks_adap_mock.get.assert_called_once_with(
            '/traits?name=in:' + ','.join(traits),
            global_request_id=self.context.global_id,
            **self.trait_api_kwargs)
        self.ks_adap_mock.put.assert_called_once_with(
            '/resource_providers/%s/traits' % uuids.rp,
            json={'traits': traits, 'resource_provider_generation': 0},
            global_request_id=self.context.global_id,
            **self.trait_api_kwargs)

        # And ensure the provider tree cache was updated appropriately
        self.assertFalse(
            self.client._provider_tree.have_traits_changed(uuids.rp, traits))
        # Validate the generation
        self.assertEqual(
            1, self.client._provider_tree.data(uuids.rp).generation)

    def test_set_traits_for_provider_with_generation(self):
        traits = ['HW_NIC_OFFLOAD_UCS', 'HW_NIC_OFFLOAD_RDMA']

        # Make _ensure_traits succeed without PUTting
        get_mock = mock.Mock(status_code=200)
        get_mock.json.return_value = {'traits': traits}
        self.ks_adap_mock.get.return_value = get_mock

        # Prime the provider tree cache
        self.client._provider_tree.new_root('rp', uuids.rp, generation=0)

        # Mock the /rp/{u}/traits PUT to succeed
        put_mock = mock.Mock(status_code=200)
        put_mock.json.return_value = {'traits': traits,
                                      'resource_provider_generation': 2}
        self.ks_adap_mock.put.return_value = put_mock

        # Invoke
        self.client.set_traits_for_provider(
            self.context, uuids.rp, traits, generation=1)

        # Verify API calls
        self.ks_adap_mock.get.assert_called_once_with(
            '/traits?name=in:' + ','.join(traits),
            global_request_id=self.context.global_id,
            **self.trait_api_kwargs)
        self.ks_adap_mock.put.assert_called_once_with(
            '/resource_providers/%s/traits' % uuids.rp,
            json={'traits': traits, 'resource_provider_generation': 1},
            global_request_id=self.context.global_id,
            **self.trait_api_kwargs)

        # And ensure the provider tree cache was updated appropriately
        self.assertFalse(
            self.client._provider_tree.have_traits_changed(uuids.rp, traits))
        # Validate the generation
        self.assertEqual(
            2, self.client._provider_tree.data(uuids.rp).generation)

    def test_set_traits_for_provider_fail(self):
        traits = ['HW_NIC_OFFLOAD_UCS', 'HW_NIC_OFFLOAD_RDMA']
        get_mock = mock.Mock()
        self.ks_adap_mock.get.return_value = get_mock

        # Prime the provider tree cache
        self.client._provider_tree.new_root('rp', uuids.rp, generation=0)

        # _ensure_traits exception bubbles up
        get_mock.status_code = 400
        self.assertRaises(
            exception.TraitRetrievalFailed,
            self.client.set_traits_for_provider,
            self.context, uuids.rp, traits)
        self.ks_adap_mock.put.assert_not_called()

        get_mock.status_code = 200
        get_mock.json.return_value = {'traits': traits}

        # Conflict
        self.ks_adap_mock.put.return_value = mock.Mock(status_code=409)
        self.assertRaises(
            exception.ResourceProviderUpdateConflict,
            self.client.set_traits_for_provider,
            self.context, uuids.rp, traits)

        # Other error
        self.ks_adap_mock.put.return_value = mock.Mock(status_code=503)
        self.assertRaises(
            exception.ResourceProviderUpdateFailed,
            self.client.set_traits_for_provider,
            self.context, uuids.rp, traits)


class TestAssociations(SchedulerReportClientTestCase):
    def setUp(self):
        super(TestAssociations, self).setUp()

        self.mock_get_inv = self.useFixture(fixtures.MockPatch(
            'nova.scheduler.client.report.SchedulerReportClient.'
            '_get_inventory')).mock
        self.inv = {
            'VCPU': {'total': 16},
            'MEMORY_MB': {'total': 1024},
            'DISK_GB': {'total': 10},
        }
        self.mock_get_inv.return_value = {
            'resource_provider_generation': 41,
            'inventories': self.inv,
        }

        self.mock_get_aggs = self.useFixture(fixtures.MockPatch(
            'nova.scheduler.client.report.SchedulerReportClient.'
            '_get_provider_aggregates')).mock
        self.mock_get_aggs.return_value = report.AggInfo(
            aggregates=set([uuids.agg1]), generation=42)

        self.mock_get_traits = self.useFixture(fixtures.MockPatch(
            'nova.scheduler.client.report.SchedulerReportClient.'
            'get_provider_traits')).mock
        self.mock_get_traits.return_value = report.TraitInfo(
            traits=set(['CUSTOM_GOLD']), generation=43)

        self.mock_get_sharing = self.useFixture(fixtures.MockPatch(
            'nova.scheduler.client.report.SchedulerReportClient.'
            '_get_sharing_providers')).mock

    def assert_getters_were_called(self, uuid, sharing=True):
        self.mock_get_inv.assert_called_once_with(self.context, uuid)
        self.mock_get_aggs.assert_called_once_with(self.context, uuid)
        self.mock_get_traits.assert_called_once_with(self.context, uuid)
        if sharing:
            self.mock_get_sharing.assert_called_once_with(
                self.context, self.mock_get_aggs.return_value[0])
        self.assertIn(uuid, self.client._association_refresh_time)
        self.assertFalse(
            self.client._provider_tree.has_inventory_changed(uuid, self.inv))
        self.assertTrue(
            self.client._provider_tree.in_aggregates(uuid, [uuids.agg1]))
        self.assertFalse(
            self.client._provider_tree.in_aggregates(uuid, [uuids.agg2]))
        self.assertTrue(
            self.client._provider_tree.has_traits(uuid, ['CUSTOM_GOLD']))
        self.assertFalse(
            self.client._provider_tree.has_traits(uuid, ['CUSTOM_SILVER']))
        self.assertEqual(43, self.client._provider_tree.data(uuid).generation)

    def assert_getters_not_called(self, timer_entry=None):
        self.mock_get_inv.assert_not_called()
        self.mock_get_aggs.assert_not_called()
        self.mock_get_traits.assert_not_called()
        self.mock_get_sharing.assert_not_called()
        if timer_entry is None:
            self.assertFalse(self.client._association_refresh_time)
        else:
            self.assertIn(timer_entry, self.client._association_refresh_time)

    def reset_getter_mocks(self):
        self.mock_get_inv.reset_mock()
        self.mock_get_aggs.reset_mock()
        self.mock_get_traits.reset_mock()
        self.mock_get_sharing.reset_mock()

    def test_refresh_associations_no_last(self):
        """Test that associations are refreshed when stale."""
        uuid = uuids.compute_node
        # Seed the provider tree so _refresh_associations finds the provider
        self.client._provider_tree.new_root('compute', uuid, generation=1)
        self.client._refresh_associations(self.context, uuid)
        self.assert_getters_were_called(uuid)

    def test_refresh_associations_no_refresh_sharing(self):
        """Test refresh_sharing=False."""
        uuid = uuids.compute_node
        # Seed the provider tree so _refresh_associations finds the provider
        self.client._provider_tree.new_root('compute', uuid, generation=1)
        self.client._refresh_associations(self.context, uuid,
                                          refresh_sharing=False)
        self.assert_getters_were_called(uuid, sharing=False)

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_associations_stale')
    def test_refresh_associations_not_stale(self, mock_stale):
        """Test that refresh associations is not called when the map is
        not stale.
        """
        mock_stale.return_value = False
        uuid = uuids.compute_node
        self.client._refresh_associations(self.context, uuid)
        self.assert_getters_not_called()

    @mock.patch('time.time', return_value=time.time())
    @mock.patch.object(report.LOG, 'debug')
    def test_refresh_associations_time(self, log_mock, mock_time):
        """Test that refresh associations is called when the map is stale."""
        uuid = uuids.compute_node
        # Seed the provider tree so _refresh_associations finds the provider
        self.client._provider_tree.new_root('compute', uuid, generation=1)

        # Called a first time because association_refresh_time is empty.
        now = mock_time.return_value
        self.client._refresh_associations(self.context, uuid)
        self.assert_getters_were_called(uuid)
        log_mock.assert_has_calls([
            mock.call('Refreshing inventories for resource provider %s', uuid),
            mock.call('Updating ProviderTree inventory for provider %s from '
                      '_refresh_and_get_inventory using data: %s',
                      uuid, self.inv),
            mock.call('Refreshing aggregate associations for resource '
                      'provider %s, aggregates: %s', uuid, uuids.agg1),
            mock.call('Refreshing trait associations for resource '
                      'provider %s, traits: %s', uuid, 'CUSTOM_GOLD')
        ])

        # Clear call count.
        self.reset_getter_mocks()

        # Not called a second time because not enough time has passed.
        mock_time.return_value = (now +
            CONF.compute.resource_provider_association_refresh / 2)
        self.client._refresh_associations(self.context, uuid)
        self.assert_getters_not_called(timer_entry=uuid)

        # Called because time has passed.
        mock_time.return_value = (now +
            CONF.compute.resource_provider_association_refresh + 1)
        self.client._refresh_associations(self.context, uuid)
        self.assert_getters_were_called(uuid)

    @mock.patch('time.time', return_value=time.time())
    def test_refresh_associations_disabled(self, mock_time):
        """Test that refresh associations can be disabled."""
        self.flags(resource_provider_association_refresh=0, group='compute')
        uuid = uuids.compute_node
        # Seed the provider tree so _refresh_associations finds the provider
        self.client._provider_tree.new_root('compute', uuid, generation=1)

        # Called a first time because association_refresh_time is empty.
        now = mock_time.return_value
        self.client._refresh_associations(self.context, uuid)
        self.assert_getters_were_called(uuid)

        # Clear call count.
        self.reset_getter_mocks()

        # A lot of time passes
        mock_time.return_value = now + 10000000000000
        self.client._refresh_associations(self.context, uuid)
        self.assert_getters_not_called(timer_entry=uuid)

        self.reset_getter_mocks()

        # Forever passes
        mock_time.return_value = float('inf')
        self.client._refresh_associations(self.context, uuid)
        self.assert_getters_not_called(timer_entry=uuid)

        self.reset_getter_mocks()

        # Even if no time passes, clearing the counter triggers refresh
        mock_time.return_value = now
        del self.client._association_refresh_time[uuid]
        self.client._refresh_associations(self.context, uuid)
        self.assert_getters_were_called(uuid)


class TestAllocations(SchedulerReportClientTestCase):

    @mock.patch("nova.scheduler.client.report.SchedulerReportClient."
                "remove_provider_tree_from_instance_allocation")
    @mock.patch('nova.objects.MigrationList.get_by_filters')
    def test_remove_allocations_for_evacuated_instances(self,
            mock_get_migrations, mock_rm_pr_tree):
        cn = objects.ComputeNode(uuid=uuids.cn, host="fake_host",
                hypervisor_hostname="fake_hostname", )
        migrations = [mock.Mock(instance_uuid=uuids.inst1, status='done'),
                      mock.Mock(instance_uuid=uuids.inst2, status='done')]
        mock_get_migrations.return_value = migrations
        mock_rm_pr_tree.return_value = True
        self.client._remove_allocations_for_evacuated_instances(self.context,
                                                                cn)
        mock_get_migrations.assert_called_once_with(
            self.context,
            {'source_compute': cn.host, 'status': ['done'],
             'migration_type': 'evacuation'})
        mock_rm_pr_tree.assert_has_calls(
            [mock.call(self.context, uuids.inst1, cn.uuid),
             mock.call(self.context, uuids.inst2, cn.uuid)])
        # status of migrations should be kept
        for migration in migrations:
            self.assertEqual('done', migration.status)

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_providers_in_tree')
    @mock.patch("nova.scheduler.client.report.SchedulerReportClient."
                "delete")
    @mock.patch("nova.scheduler.client.report.SchedulerReportClient."
                "_remove_allocations_for_evacuated_instances")
    @mock.patch("nova.scheduler.client.report.SchedulerReportClient."
                "delete_allocation_for_instance")
    @mock.patch("nova.objects.InstanceList.get_uuids_by_host_and_node")
    def test_delete_resource_provider_cascade(self, mock_by_host,
            mock_del_alloc, mock_evacuate, mock_delete, mock_get_rpt):
        cn = objects.ComputeNode(uuid=uuids.cn, host="fake_host",
                hypervisor_hostname="fake_hostname", )
        mock_by_host.return_value = [uuids.inst1, uuids.inst2]
        mock_get_rpt.return_value = [{
            'uuid': cn.uuid,
            'name': mock.sentinel.name,
            'generation': 1,
            'parent_provider_uuid': None
        }]
        resp_mock = mock.Mock(status_code=204)
        mock_delete.return_value = resp_mock
        self.client.delete_resource_provider(self.context, cn, cascade=True)
        mock_by_host.assert_called_once_with(
            self.context, cn.host, cn.hypervisor_hostname)
        self.assertEqual(2, mock_del_alloc.call_count)
        mock_evacuate.assert_called_once_with(self.context, cn)
        exp_url = "/resource_providers/%s" % uuids.cn
        mock_delete.assert_called_once_with(
            exp_url, global_request_id=self.context.global_id)
        self.assertFalse(self.client._provider_tree.exists(uuids.cn))

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_providers_in_tree')
    @mock.patch("nova.scheduler.client.report.SchedulerReportClient."
                "delete")
    @mock.patch("nova.scheduler.client.report.SchedulerReportClient."
                "_remove_allocations_for_evacuated_instances")
    @mock.patch("nova.scheduler.client.report.SchedulerReportClient."
                "delete_allocation_for_instance")
    @mock.patch("nova.objects.InstanceList.get_uuids_by_host_and_node")
    def test_delete_resource_provider_no_cascade(self, mock_by_host,
            mock_del_alloc, mock_evacuate, mock_delete, mock_get_rpt):
        self.client._association_refresh_time[uuids.cn] = mock.Mock()
        cn = objects.ComputeNode(uuid=uuids.cn, host="fake_host",
                hypervisor_hostname="fake_hostname", )
        mock_get_rpt.return_value = [{
            'uuid': cn.uuid,
            'name': mock.sentinel.name,
            'generation': 1,
            'parent_provider_uuid': None
        }]
        mock_by_host.return_value = [uuids.inst1, uuids.inst2]
        resp_mock = mock.Mock(status_code=204)
        mock_delete.return_value = resp_mock
        self.client.delete_resource_provider(self.context, cn)
        mock_del_alloc.assert_not_called()
        mock_evacuate.assert_not_called()
        exp_url = "/resource_providers/%s" % uuids.cn
        mock_delete.assert_called_once_with(
            exp_url, global_request_id=self.context.global_id)
        self.assertNotIn(uuids.cn, self.client._association_refresh_time)

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_providers_in_tree')
    @mock.patch("nova.scheduler.client.report.SchedulerReportClient."
                "delete")
    @mock.patch('nova.scheduler.client.report.LOG')
    def test_delete_resource_provider_log_calls(self, mock_log, mock_delete,
                                                get_rpt_mock):
        cn = objects.ComputeNode(uuid=uuids.cn, host="fake_host",
                hypervisor_hostname="fake_hostname", )
        get_rpt_mock.return_value = [{
            'uuid': cn.uuid,
            'name': mock.sentinel.name,
            'generation': 1,
            'parent_provider_uuid': None
        }]
        resp_mock = fake_requests.FakeResponse(204)
        mock_delete.return_value = resp_mock
        self.client.delete_resource_provider(self.context, cn)
        # With a 204, only the info should be called
        self.assertEqual(1, mock_log.info.call_count)
        self.assertEqual(0, mock_log.warning.call_count)

        # Now check a 404 response
        mock_log.reset_mock()
        resp_mock.status_code = 404
        self.client.delete_resource_provider(self.context, cn)
        # With a 404, neither log message should be called
        self.assertEqual(0, mock_log.info.call_count)
        self.assertEqual(0, mock_log.warning.call_count)

        # Finally, check a 409 response
        mock_log.reset_mock()
        resp_mock.status_code = 409
        self.client.delete_resource_provider(self.context, cn)
        # With a 409, only the error should be called
        self.assertEqual(0, mock_log.info.call_count)
        self.assertEqual(1, mock_log.error.call_count)

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_providers_in_tree')
    @mock.patch("nova.scheduler.client.report.SchedulerReportClient."
                "delete")
    @mock.patch('nova.scheduler.client.report.LOG')
    def test_delete_resource_providers_by_order(self, mock_log, mock_delete,
                                                mock_get_rpt):
        """Ensure that more than on RP is in the tree and that all of them
         is gets deleted in the proper order.
        """
        cn = objects.ComputeNode(uuid=uuids.cn, host="fake_host",
                hypervisor_hostname="fake_hostname", )
        mock_get_rpt.return_value = [
            {
                'uuid': uuids.child1,
                'name': mock.sentinel.name,
                'generation': 1,
                'parent_provider_uuid': cn.uuid
            },
            {
                'uuid': uuids.gc1_1,
                'name': mock.sentinel.name,
                'generation': 1,
                'parent_provider_uuid': uuids.child1
            },
            {
                'uuid': cn.uuid,
                'name': mock.sentinel.name,
                'generation': 1,
                'parent_provider_uuid': None
            }
        ]
        mock_delete.return_value = True
        self.client.delete_resource_provider(self.context, cn)
        self.assertEqual(3, mock_delete.call_count)
        # Delete RP in correct order
        mock_delete.assert_has_calls([
            mock.call('/resource_providers/%s' % uuids.gc1_1,
                      global_request_id=mock.ANY),
            mock.call('/resource_providers/%s' % uuids.child1,
                      global_request_id=mock.ANY),
            mock.call('/resource_providers/%s' % cn.uuid,
                      global_request_id=mock.ANY),
        ])
        exp_url = "Deleted resource provider %s"
        # Logging info in correct order: uuids.gc1_1, uuids.child1, cn.uuid
        mock_log.assert_has_calls([
            mock.call.info(exp_url, uuids.gc1_1),
            mock.call.info(exp_url, uuids.child1),
            mock.call.info(exp_url, cn.uuid),
        ])

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_providers_in_tree')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.delete',
                new=mock.Mock(side_effect=ks_exc.EndpointNotFound()))
    def test_delete_resource_provider_placement_exception(self,
                                                          mock_get_rpt):
        """Ensure that a ksa exception in delete_resource_provider raises
        through.
        """
        cn = objects.ComputeNode(uuid=uuids.cn, host="fake_host",
                hypervisor_hostname="fake_hostname", )
        mock_get_rpt.return_value = [{
            'uuid': cn.uuid,
            'name': mock.sentinel.name,
            'generation': 1,
            'parent_provider_uuid': None
        }]
        self.assertRaises(
            ks_exc.ClientException,
            self.client.delete_resource_provider, self.context, cn)

    @mock.patch("nova.scheduler.client.report.SchedulerReportClient.get")
    def test_get_allocations_for_resource_provider(self, mock_get):
        mock_get.return_value = fake_requests.FakeResponse(
            200, content=jsonutils.dumps(
                {'allocations': 'fake', 'resource_provider_generation': 42}))
        ret = self.client.get_allocations_for_resource_provider(
            self.context, 'rpuuid')
        self.assertEqual('fake', ret.allocations)
        mock_get.assert_called_once_with(
            '/resource_providers/rpuuid/allocations',
            global_request_id=self.context.global_id)

    @mock.patch("nova.scheduler.client.report.SchedulerReportClient.get")
    def test_get_allocations_for_resource_provider_fail(self, mock_get):
        mock_get.return_value = fake_requests.FakeResponse(400, content="ouch")
        self.assertRaises(exception.ResourceProviderAllocationRetrievalFailed,
                          self.client.get_allocations_for_resource_provider,
                          self.context, 'rpuuid')
        mock_get.assert_called_once_with(
            '/resource_providers/rpuuid/allocations',
            global_request_id=self.context.global_id)

    @mock.patch("nova.scheduler.client.report.SchedulerReportClient.get")
    def test_get_allocs_for_consumer(self, mock_get):
        mock_get.return_value = fake_requests.FakeResponse(
            200, content=jsonutils.dumps({'foo': 'bar'}))
        ret = self.client.get_allocs_for_consumer(self.context, 'consumer')
        self.assertEqual({'foo': 'bar'}, ret)
        mock_get.assert_called_once_with(
            '/allocations/consumer', version='1.28',
            global_request_id=self.context.global_id)

    @mock.patch("nova.scheduler.client.report.SchedulerReportClient.get")
    def test_get_allocs_for_consumer_fail(self, mock_get):
        mock_get.return_value = fake_requests.FakeResponse(400, content='err')
        self.assertRaises(exception.ConsumerAllocationRetrievalFailed,
                          self.client.get_allocs_for_consumer,
                          self.context, 'consumer')
        mock_get.assert_called_once_with(
            '/allocations/consumer', version='1.28',
            global_request_id=self.context.global_id)

    @mock.patch("nova.scheduler.client.report.SchedulerReportClient.get")
    def test_get_allocs_for_consumer_safe_connect_fail(self, mock_get):
        mock_get.side_effect = ks_exc.EndpointNotFound()
        self.assertRaises(ks_exc.ClientException,
                          self.client.get_allocs_for_consumer,
                          self.context, 'consumer')
        mock_get.assert_called_once_with(
            '/allocations/consumer', version='1.28',
            global_request_id=self.context.global_id)

    def _test_remove_res_from_alloc(
            self, current_allocations, resources_to_remove,
            updated_allocations):

        with test.nested(
                mock.patch(
                    "nova.scheduler.client.report.SchedulerReportClient.get"),
                mock.patch(
                    "nova.scheduler.client.report.SchedulerReportClient.put")
        ) as (mock_get, mock_put):
            mock_get.return_value = fake_requests.FakeResponse(
                200, content=jsonutils.dumps(current_allocations))

            self.client.remove_resources_from_instance_allocation(
                self.context, uuids.consumer_uuid, resources_to_remove)

            mock_get.assert_called_once_with(
                '/allocations/%s' % uuids.consumer_uuid, version='1.28',
                global_request_id=self.context.global_id)
            mock_put.assert_called_once_with(
                '/allocations/%s' % uuids.consumer_uuid, updated_allocations,
                version='1.28', global_request_id=self.context.global_id)

    def test_remove_res_from_alloc(self):
        current_allocations = {
            "allocations": {
                uuids.rp1: {
                    "generation": 13,
                    "resources": {
                        'VCPU': 10,
                        'MEMORY_MB': 4096,
                    },
                },
                uuids.rp2: {
                    "generation": 42,
                    "resources": {
                        'NET_BW_EGR_KILOBIT_PER_SEC': 200,
                        'NET_BW_IGR_KILOBIT_PER_SEC': 300,
                    },
                },
            },
            "consumer_generation": 2,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id,
        }
        resources_to_remove = {

            uuids.rp1: {
                "resources": {
                    'VCPU': 1,
                },
            },
            uuids.rp2: {
                "resources": {
                    'NET_BW_EGR_KILOBIT_PER_SEC': 100,
                    'NET_BW_IGR_KILOBIT_PER_SEC': 200,
                },
            }
        }
        updated_allocations = {
            "allocations": {
                uuids.rp1: {
                    "generation": 13,
                    "resources": {
                        'VCPU': 9,
                        'MEMORY_MB': 4096,
                    },
                },
                uuids.rp2: {
                    "generation": 42,
                    "resources": {
                        'NET_BW_EGR_KILOBIT_PER_SEC': 100,
                        'NET_BW_IGR_KILOBIT_PER_SEC': 100,
                    },
                },
            },
            "consumer_generation": 2,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id,
        }

        self._test_remove_res_from_alloc(
            current_allocations, resources_to_remove, updated_allocations)

    def test_remove_res_from_alloc_remove_rc_when_value_dropped_to_zero(self):
        current_allocations = {
            "allocations": {
                uuids.rp1: {
                    "generation": 42,
                    "resources": {
                        'NET_BW_EGR_KILOBIT_PER_SEC': 200,
                        'NET_BW_IGR_KILOBIT_PER_SEC': 300,
                    },
                },
            },
            "consumer_generation": 2,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id,
        }
        # this will remove all of NET_BW_EGR_KILOBIT_PER_SEC resources from
        # the allocation so the whole resource class will be removed
        resources_to_remove = {
            uuids.rp1: {
                "resources": {
                    'NET_BW_EGR_KILOBIT_PER_SEC': 200,
                    'NET_BW_IGR_KILOBIT_PER_SEC': 200,
                }
            }
        }
        updated_allocations = {
            "allocations": {
                uuids.rp1: {
                    "generation": 42,
                    "resources": {
                        'NET_BW_IGR_KILOBIT_PER_SEC': 100,
                    },
                },
            },
            "consumer_generation": 2,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id,
        }

        self._test_remove_res_from_alloc(
            current_allocations, resources_to_remove, updated_allocations)

    def test_remove_res_from_alloc_remove_rp_when_all_rc_removed(self):
        current_allocations = {
            "allocations": {
                uuids.rp1: {
                    "generation": 42,
                    "resources": {
                        'NET_BW_EGR_KILOBIT_PER_SEC': 200,
                        'NET_BW_IGR_KILOBIT_PER_SEC': 300,
                    },
                },
            },
            "consumer_generation": 2,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id,
        }
        resources_to_remove = {
            uuids.rp1: {
                "resources": {
                    'NET_BW_EGR_KILOBIT_PER_SEC': 200,
                    'NET_BW_IGR_KILOBIT_PER_SEC': 300,
                }
            }
        }
        updated_allocations = {
            "allocations": {},
            "consumer_generation": 2,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id,
        }

        self._test_remove_res_from_alloc(
            current_allocations, resources_to_remove, updated_allocations)

    @mock.patch("nova.scheduler.client.report.SchedulerReportClient.get")
    def test_remove_res_from_alloc_failed_to_get_alloc(
            self, mock_get):
        mock_get.side_effect = ks_exc.EndpointNotFound()
        resources_to_remove = {
            uuids.rp1: {
                "resources": {
                    'NET_BW_EGR_KILOBIT_PER_SEC': 200,
                    'NET_BW_IGR_KILOBIT_PER_SEC': 200,
                }
            }
        }

        self.assertRaises(
            ks_exc.ClientException,
            self.client.remove_resources_from_instance_allocation,
            self.context, uuids.consumer_uuid, resources_to_remove)

    def test_remove_res_from_alloc_empty_alloc(self):
        resources_to_remove = {
            uuids.rp1: {
                "resources": {
                    'NET_BW_EGR_KILOBIT_PER_SEC': 200,
                    'NET_BW_IGR_KILOBIT_PER_SEC': 200,
                }
            }
        }
        current_allocations = {
            "allocations": {},
            "consumer_generation": 0,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id,
        }
        ex = self.assertRaises(
            exception.AllocationUpdateFailed,
            self._test_remove_res_from_alloc, current_allocations,
            resources_to_remove, None)
        self.assertIn('The allocation is empty', str(ex))

    @mock.patch("nova.scheduler.client.report.SchedulerReportClient.put")
    @mock.patch("nova.scheduler.client.report.SchedulerReportClient.get")
    def test_remove_res_from_alloc_no_resource_to_remove(
            self, mock_get, mock_put):
        self.client.remove_resources_from_instance_allocation(
            self.context, uuids.consumer_uuid, {})

        mock_get.assert_not_called()
        mock_put.assert_not_called()

    def test_remove_res_from_alloc_missing_rc(self):
        current_allocations = {
            "allocations": {
                uuids.rp1: {
                    "generation": 42,
                    "resources": {
                        'NET_BW_EGR_KILOBIT_PER_SEC': 200,
                    },
                },
            },
            "consumer_generation": 2,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id,
        }
        resources_to_remove = {
            uuids.rp1: {
                "resources": {
                    'VCPU': 1,
                }
            }
        }

        ex = self.assertRaises(
            exception.AllocationUpdateFailed, self._test_remove_res_from_alloc,
            current_allocations, resources_to_remove, None)
        self.assertIn("Key 'VCPU' is missing from the allocation", str(ex))

    def test_remove_res_from_alloc_missing_rp(self):
        current_allocations = {
            "allocations": {
                uuids.rp1: {
                    "generation": 42,
                    "resources": {
                        'NET_BW_EGR_KILOBIT_PER_SEC': 200,
                    },
                },
            },
            "consumer_generation": 2,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id,
        }
        resources_to_remove = {
            uuids.other_rp: {
                "resources": {
                    'NET_BW_EGR_KILOBIT_PER_SEC': 200,
                }
            }
        }

        ex = self.assertRaises(
            exception.AllocationUpdateFailed, self._test_remove_res_from_alloc,
            current_allocations, resources_to_remove, None)
        self.assertIn(
            "Key '%s' is missing from the allocation" % uuids.other_rp,
            str(ex))

    def test_remove_res_from_alloc_not_enough_resource_to_remove(self):
        current_allocations = {
            "allocations": {
                uuids.rp1: {
                    "generation": 42,
                    "resources": {
                        'NET_BW_EGR_KILOBIT_PER_SEC': 200,
                    },
                },
            },
            "consumer_generation": 2,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id,
        }
        resources_to_remove = {
            uuids.rp1: {
                "resources": {
                    'NET_BW_EGR_KILOBIT_PER_SEC': 400,
                }
            }
        }

        ex = self.assertRaises(
            exception.AllocationUpdateFailed, self._test_remove_res_from_alloc,
            current_allocations, resources_to_remove, None)
        self.assertIn(
            'There are not enough allocated resources left on %s resource '
            'provider to remove 400 amount of NET_BW_EGR_KILOBIT_PER_SEC '
            'resources' %
            uuids.rp1,
            str(ex))

    @mock.patch('time.sleep', new=mock.Mock())
    @mock.patch("nova.scheduler.client.report.SchedulerReportClient.put")
    @mock.patch("nova.scheduler.client.report.SchedulerReportClient.get")
    def test_remove_res_from_alloc_retry_succeed(
            self, mock_get, mock_put):
        current_allocations = {
            "allocations": {
                uuids.rp1: {
                    "generation": 42,
                    "resources": {
                        'NET_BW_EGR_KILOBIT_PER_SEC': 200,
                    },
                },
            },
            "consumer_generation": 2,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id,
        }
        current_allocations_2 = copy.deepcopy(current_allocations)
        current_allocations_2['consumer_generation'] = 3
        resources_to_remove = {
            uuids.rp1: {
                "resources": {
                    'NET_BW_EGR_KILOBIT_PER_SEC': 200,
                }
            }
        }
        updated_allocations = {
            "allocations": {},
            "consumer_generation": 2,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id,
        }
        updated_allocations_2 = copy.deepcopy(updated_allocations)
        updated_allocations_2['consumer_generation'] = 3
        mock_get.side_effect = [
            fake_requests.FakeResponse(
                200, content=jsonutils.dumps(current_allocations)),
            fake_requests.FakeResponse(
                200, content=jsonutils.dumps(current_allocations_2))
        ]

        mock_put.side_effect = [
            fake_requests.FakeResponse(
                status_code=409,
                content=jsonutils.dumps(
                    {'errors': [{'code': 'placement.concurrent_update',
                                 'detail': ''}]})),
            fake_requests.FakeResponse(
                status_code=204)
        ]

        self.client.remove_resources_from_instance_allocation(
            self.context, uuids.consumer_uuid, resources_to_remove)

        self.assertEqual(
            [
                mock.call(
                    '/allocations/%s' % uuids.consumer_uuid, version='1.28',
                    global_request_id=self.context.global_id),
                mock.call(
                    '/allocations/%s' % uuids.consumer_uuid, version='1.28',
                    global_request_id=self.context.global_id)
            ],
            mock_get.mock_calls)

        self.assertEqual(
            [
                mock.call(
                    '/allocations/%s' % uuids.consumer_uuid,
                    updated_allocations, version='1.28',
                    global_request_id=self.context.global_id),
                mock.call(
                    '/allocations/%s' % uuids.consumer_uuid,
                    updated_allocations_2, version='1.28',
                    global_request_id=self.context.global_id),
            ],
            mock_put.mock_calls)

    @mock.patch('time.sleep', new=mock.Mock())
    @mock.patch("nova.scheduler.client.report.SchedulerReportClient.put")
    @mock.patch("nova.scheduler.client.report.SchedulerReportClient.get")
    def test_remove_res_from_alloc_run_out_of_retries(
            self, mock_get, mock_put):
        current_allocations = {
            "allocations": {
                uuids.rp1: {
                    "generation": 42,
                    "resources": {
                        'NET_BW_EGR_KILOBIT_PER_SEC': 200,
                    },
                },
            },
            "consumer_generation": 2,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id,
        }
        resources_to_remove = {
            uuids.rp1: {
                "resources": {
                    'NET_BW_EGR_KILOBIT_PER_SEC': 200,
                }
            }
        }
        updated_allocations = {
            "allocations": {},
            "consumer_generation": 2,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id,
        }

        get_rsp = fake_requests.FakeResponse(
            200, content=jsonutils.dumps(current_allocations))

        mock_get.side_effect = [get_rsp] * 4

        put_rsp = fake_requests.FakeResponse(
            status_code=409,
            content=jsonutils.dumps(
                    {'errors': [{'code': 'placement.concurrent_update',
                                 'detail': ''}]}))

        mock_put.side_effect = [put_rsp] * 4

        ex = self.assertRaises(
            exception.AllocationUpdateFailed,
            self.client.remove_resources_from_instance_allocation,
            self.context, uuids.consumer_uuid, resources_to_remove)
        self.assertIn(
            'due to multiple successive generation conflicts',
            str(ex))

        get_call = mock.call(
            '/allocations/%s' % uuids.consumer_uuid, version='1.28',
            global_request_id=self.context.global_id)

        mock_get.assert_has_calls([get_call] * 4)

        put_call = mock.call(
            '/allocations/%s' % uuids.consumer_uuid, updated_allocations,
            version='1.28', global_request_id=self.context.global_id)

        mock_put.assert_has_calls([put_call] * 4)

    def _test_add_res_to_alloc(
        self, current_allocations, resources_to_add, updated_allocations):

        with test.nested(
            mock.patch.object(self.client, 'get'),
            mock.patch.object(self.client, 'put'),
        ) as (mock_get, mock_put):
            mock_get.return_value = fake_requests.FakeResponse(
                200, content=jsonutils.dumps(current_allocations))
            mock_put.return_value = fake_requests.FakeResponse(204)

            self.client.add_resources_to_instance_allocation(
                self.context, uuids.consumer_uuid, resources_to_add)

            mock_get.assert_called_once_with(
                '/allocations/%s' % uuids.consumer_uuid, version='1.28',
                global_request_id=self.context.global_id)
            mock_put.assert_called_once_with(
                '/allocations/%s' % uuids.consumer_uuid, updated_allocations,
                version='1.28', global_request_id=self.context.global_id)

    @mock.patch("nova.scheduler.client.report.SchedulerReportClient.put")
    @mock.patch("nova.scheduler.client.report.SchedulerReportClient.get")
    def test_add_res_to_alloc_empty_addition(self, mock_get, mock_put):
        self.client.add_resources_to_instance_allocation(
            self.context, uuids.consumer_uuid, {})

        mock_get.assert_not_called()
        mock_put.assert_not_called()

    def test_add_res_to_alloc(self):
        current_allocation = {
            "allocations": {
                uuids.rp1: {
                    "generation": 42,
                    "resources": {
                        'NET_BW_EGR_KILOBIT_PER_SEC': 200,
                    },
                },
            },
            "consumer_generation": 2,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id,
        }
        addition = {
            uuids.rp1: {
                "resources": {
                    "FOO": 1,  # existing RP but new resource class
                    "NET_BW_EGR_KILOBIT_PER_SEC": 100,  # existing PR and rc
                },
            },
            uuids.rp2: {  # new RP
                "resources": {
                    "BAR": 1,
                },
            },
        }
        expected_allocation = {
            "allocations": {
                uuids.rp1: {
                    "generation": 42,
                    "resources": {
                        "FOO": 1,
                        "NET_BW_EGR_KILOBIT_PER_SEC": 200 + 100,
                    },
                },
                uuids.rp2: {
                    "resources": {
                        "BAR": 1,
                    },
                },
            },
            "consumer_generation": 2,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id,
        }

        self._test_add_res_to_alloc(
            current_allocation, addition, expected_allocation)

    @mock.patch("nova.scheduler.client.report.SchedulerReportClient.get")
    def test_add_res_to_alloc_failed_to_get_alloc(self, mock_get):
        mock_get.side_effect = ks_exc.EndpointNotFound()
        addition = {
            uuids.rp1: {
                "resources": {
                    "NET_BW_EGR_KILOBIT_PER_SEC": 200,
                    "NET_BW_IGR_KILOBIT_PER_SEC": 200,
                }
            }
        }

        self.assertRaises(
            ks_exc.ClientException,
            self.client.add_resources_to_instance_allocation,
            self.context, uuids.consumer_uuid, addition)

    @mock.patch("nova.scheduler.client.report.SchedulerReportClient.put")
    @mock.patch("nova.scheduler.client.report.SchedulerReportClient.get")
    def test_add_res_to_alloc_failed_to_put_alloc_non_conflict(
        self, mock_get, mock_put):

        current_allocations = {
            "allocations": {
                uuids.rp1: {
                    "generation": 42,
                    "resources": {
                        'NET_BW_EGR_KILOBIT_PER_SEC': 200,
                    },
                },
            },
            "consumer_generation": 2,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id,
        }
        mock_get.side_effect = [
            fake_requests.FakeResponse(
                200, content=jsonutils.dumps(current_allocations)),
        ]
        addition = {
            uuids.rp1: {
                "resources": {
                    "NET_BW_EGR_KILOBIT_PER_SEC": 200,
                    "NET_BW_IGR_KILOBIT_PER_SEC": 200,
                }
            }
        }
        mock_put.side_effect = [
            fake_requests.FakeResponse(
                404,
                content=jsonutils.dumps(
                    {'errors': [
                        {'code': 'placement.undefined_code', 'detail': ''}]}))
            ]

        self.assertRaises(
            exception.AllocationUpdateFailed,
            self.client.add_resources_to_instance_allocation,
            self.context, uuids.consumer_uuid, addition)

    @mock.patch('time.sleep', new=mock.Mock())
    @mock.patch("nova.scheduler.client.report.SchedulerReportClient.put")
    @mock.patch("nova.scheduler.client.report.SchedulerReportClient.get")
    def test_add_res_to_alloc_retry_succeed(self, mock_get, mock_put):
        current_allocations = {
            "allocations": {
                uuids.rp1: {
                    "generation": 42,
                    "resources": {
                        'NET_BW_EGR_KILOBIT_PER_SEC': 200,
                    },
                },
            },
            "consumer_generation": 2,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id,
        }
        current_allocations_2 = copy.deepcopy(current_allocations)
        current_allocations_2['consumer_generation'] = 3
        addition = {
            uuids.rp1: {
                "resources": {
                    "NET_BW_EGR_KILOBIT_PER_SEC": 100,
                }
            }
        }
        updated_allocations = {
            "allocations": {
                uuids.rp1: {
                    "generation": 42,
                    "resources": {
                        'NET_BW_EGR_KILOBIT_PER_SEC': 200 + 100,
                    },
                },
            },
            "consumer_generation": 2,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id,
        }
        updated_allocations_2 = copy.deepcopy(updated_allocations)
        updated_allocations_2['consumer_generation'] = 3
        mock_get.side_effect = [
            fake_requests.FakeResponse(
                200, content=jsonutils.dumps(current_allocations)),
            fake_requests.FakeResponse(
                200, content=jsonutils.dumps(current_allocations_2))
        ]

        mock_put.side_effect = [
            fake_requests.FakeResponse(
                status_code=409,
                content=jsonutils.dumps(
                    {'errors': [{'code': 'placement.concurrent_update',
                                 'detail': ''}]})),
            fake_requests.FakeResponse(
                status_code=204)
        ]

        self.client.add_resources_to_instance_allocation(
            self.context, uuids.consumer_uuid, addition)

        self.assertEqual(
            [
                mock.call(
                    '/allocations/%s' % uuids.consumer_uuid,
                    version='1.28',
                    global_request_id=self.context.global_id),
                mock.call(
                    '/allocations/%s' % uuids.consumer_uuid,
                    version='1.28',
                    global_request_id=self.context.global_id)
            ],
            mock_get.mock_calls)

        self.assertEqual(
            [
                mock.call(
                    '/allocations/%s' % uuids.consumer_uuid,
                    updated_allocations, version='1.28',
                    global_request_id=self.context.global_id),
                mock.call(
                    '/allocations/%s' % uuids.consumer_uuid,
                    updated_allocations_2, version='1.28',
                    global_request_id=self.context.global_id),
            ],
            mock_put.mock_calls)

    @mock.patch('time.sleep', new=mock.Mock())
    @mock.patch("nova.scheduler.client.report.SchedulerReportClient.put")
    @mock.patch("nova.scheduler.client.report.SchedulerReportClient.get")
    def test_add_res_to_alloc_run_out_of_retries(self, mock_get, mock_put):
        current_allocations = {
            "allocations": {
                uuids.rp1: {
                    "generation": 42,
                    "resources": {
                        'NET_BW_EGR_KILOBIT_PER_SEC': 200,
                    },
                },
            },
            "consumer_generation": 2,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id,
        }
        addition = {
            uuids.rp1: {
                "resources": {
                    "NET_BW_EGR_KILOBIT_PER_SEC": 100,
                }
            }
        }
        updated_allocations = {
            "allocations": {
                uuids.rp1: {
                    "generation": 42,
                    "resources": {
                        'NET_BW_EGR_KILOBIT_PER_SEC': 200 + 100,
                    },
                },
            },
            "consumer_generation": 2,
            "project_id": uuids.project_id,
            "user_id": uuids.user_id,
        }

        get_rsp = fake_requests.FakeResponse(
            200, content=jsonutils.dumps(current_allocations))

        mock_get.side_effect = [get_rsp] * 4

        put_rsp = fake_requests.FakeResponse(
            status_code=409,
            content=jsonutils.dumps(
                    {'errors': [{'code': 'placement.concurrent_update',
                                 'detail': ''}]}))

        mock_put.side_effect = [put_rsp] * 4

        ex = self.assertRaises(
            exception.AllocationUpdateFailed,
            self.client.add_resources_to_instance_allocation,
            self.context, uuids.consumer_uuid, addition)
        self.assertIn(
            'due to multiple successive generation conflicts',
            str(ex))

        get_call = mock.call(
            '/allocations/%s' % uuids.consumer_uuid, version='1.28',
            global_request_id=self.context.global_id)

        mock_get.assert_has_calls([get_call] * 4)

        put_call = mock.call(
            '/allocations/%s' % uuids.consumer_uuid, updated_allocations,
            version='1.28', global_request_id=self.context.global_id)

        mock_put.assert_has_calls([put_call] * 4)


class TestResourceClass(SchedulerReportClientTestCase):
    def setUp(self):
        super(TestResourceClass, self).setUp()
        _put_patch = mock.patch(
            "nova.scheduler.client.report.SchedulerReportClient.put")
        self.addCleanup(_put_patch.stop)
        self.mock_put = _put_patch.start()

    def test_ensure_resource_classes(self):
        rcs = ['VCPU', 'CUSTOM_FOO', 'MEMORY_MB', 'CUSTOM_BAR']
        self.client._ensure_resource_classes(self.context, rcs)
        self.mock_put.assert_has_calls([
            mock.call('/resource_classes/%s' % rc, None, version='1.7',
                      global_request_id=self.context.global_id)
            for rc in ('CUSTOM_FOO', 'CUSTOM_BAR')
        ], any_order=True)

    def test_ensure_resource_classes_none(self):
        for empty in ([], (), set(), {}):
            self.client._ensure_resource_classes(self.context, empty)
            self.mock_put.assert_not_called()

    def test_ensure_resource_classes_put_fail(self):
        self.mock_put.return_value = fake_requests.FakeResponse(503)
        rcs = ['VCPU', 'MEMORY_MB', 'CUSTOM_BAD']
        self.assertRaises(
            exception.InvalidResourceClass,
            self.client._ensure_resource_classes, self.context, rcs)
        # Only called with the "bad" one
        self.mock_put.assert_called_once_with(
            '/resource_classes/CUSTOM_BAD', None, version='1.7',
            global_request_id=self.context.global_id)


class TestAggregateAddRemoveHost(SchedulerReportClientTestCase):
    """Unit tests for the methods of the report client which look up providers
    by name and add/remove host aggregates to providers. These methods do not
    access the SchedulerReportClient provider_tree attribute and are called
    from the nova API, not the nova compute manager/resource tracker.
    """

    def setUp(self):
        super(TestAggregateAddRemoveHost, self).setUp()
        self.mock_get = self.useFixture(
            fixtures.MockPatch('nova.scheduler.client.report.'
                               'SchedulerReportClient.get')).mock
        self.mock_put = self.useFixture(
            fixtures.MockPatch('nova.scheduler.client.report.'
                               'SchedulerReportClient.put')).mock

    def test_get_provider_by_name_success(self):
        get_resp = mock.Mock()
        get_resp.status_code = 200
        get_resp.json.return_value = {
            "resource_providers": [
                mock.sentinel.expected,
            ]
        }
        self.mock_get.return_value = get_resp
        name = 'cn1'
        res = self.client.get_provider_by_name(self.context, name)

        exp_url = "/resource_providers?name=%s" % name
        self.mock_get.assert_called_once_with(
            exp_url, global_request_id=self.context.global_id)
        self.assertEqual(mock.sentinel.expected, res)

    @mock.patch.object(report.LOG, 'warning')
    def test_get_provider_by_name_multiple_results(self, mock_log):
        """Test that if we find multiple resource providers with the same name,
        that a ResourceProviderNotFound is raised (the reason being that >1
        resource provider with a name should never happen...)
        """
        get_resp = mock.Mock()
        get_resp.status_code = 200
        get_resp.json.return_value = {
            "resource_providers": [
                {'uuid': uuids.cn1a},
                {'uuid': uuids.cn1b},
            ]
        }
        self.mock_get.return_value = get_resp
        name = 'cn1'
        self.assertRaises(
            exception.ResourceProviderNotFound,
            self.client.get_provider_by_name, self.context, name)
        mock_log.assert_called_once()

    @mock.patch.object(report.LOG, 'warning')
    def test_get_provider_by_name_500(self, mock_log):
        get_resp = mock.Mock()
        get_resp.status_code = 500
        self.mock_get.return_value = get_resp
        name = 'cn1'
        self.assertRaises(
            exception.ResourceProviderNotFound,
            self.client.get_provider_by_name, self.context, name)
        mock_log.assert_called_once()

    @mock.patch.object(report.LOG, 'warning')
    def test_get_provider_by_name_404(self, mock_log):
        get_resp = mock.Mock()
        get_resp.status_code = 404
        self.mock_get.return_value = get_resp
        name = 'cn1'
        self.assertRaises(
            exception.ResourceProviderNotFound,
            self.client.get_provider_by_name, self.context, name)
        mock_log.assert_not_called()

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'set_aggregates_for_provider')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_get_provider_aggregates')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_provider_by_name')
    def test_aggregate_add_host_success_no_existing(
            self, mock_get_by_name, mock_get_aggs, mock_set_aggs):
        mock_get_by_name.return_value = {
            'uuid': uuids.cn1,
            'generation': 1,
        }
        agg_uuid = uuids.agg1
        mock_get_aggs.return_value = report.AggInfo(aggregates=set([]),
                                                    generation=42)
        name = 'cn1'
        self.client.aggregate_add_host(self.context, agg_uuid, host_name=name)
        mock_set_aggs.assert_called_once_with(
            self.context, uuids.cn1, set([agg_uuid]), use_cache=False,
            generation=42)

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'set_aggregates_for_provider')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_get_provider_aggregates')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_provider_by_name', new=mock.NonCallableMock())
    def test_aggregate_add_host_rp_uuid(self, mock_get_aggs, mock_set_aggs):
        mock_get_aggs.return_value = report.AggInfo(
            aggregates=set([]), generation=42)
        self.client.aggregate_add_host(
            self.context, uuids.agg1, rp_uuid=uuids.cn1)
        mock_set_aggs.assert_called_once_with(
            self.context, uuids.cn1, set([uuids.agg1]), use_cache=False,
            generation=42)

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'set_aggregates_for_provider')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_get_provider_aggregates')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_provider_by_name')
    def test_aggregate_add_host_success_already_existing(
            self, mock_get_by_name, mock_get_aggs, mock_set_aggs):
        mock_get_by_name.return_value = {
            'uuid': uuids.cn1,
            'generation': 1,
        }
        agg1_uuid = uuids.agg1
        agg2_uuid = uuids.agg2
        agg3_uuid = uuids.agg3
        mock_get_aggs.return_value = report.AggInfo(
            aggregates=set([agg1_uuid]), generation=42)
        name = 'cn1'
        self.client.aggregate_add_host(self.context, agg1_uuid, host_name=name)
        mock_set_aggs.assert_not_called()
        mock_get_aggs.reset_mock()
        mock_set_aggs.reset_mock()
        mock_get_aggs.return_value = report.AggInfo(
            aggregates=set([agg1_uuid, agg3_uuid]), generation=43)
        self.client.aggregate_add_host(self.context, agg2_uuid, host_name=name)
        mock_set_aggs.assert_called_once_with(
            self.context, uuids.cn1, set([agg1_uuid, agg2_uuid, agg3_uuid]),
            use_cache=False, generation=43)

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_provider_by_name',
                side_effect=exception.PlacementAPIConnectFailure)
    def test_aggregate_add_host_no_placement(self, mock_get_by_name):
        """Tests that PlacementAPIConnectFailure will be raised up from
        aggregate_add_host if get_provider_by_name raises that error.
        """
        name = 'cn1'
        agg_uuid = uuids.agg1
        self.assertRaises(
            exception.PlacementAPIConnectFailure,
            self.client.aggregate_add_host, self.context, agg_uuid,
            host_name=name)
        self.mock_get.assert_not_called()

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'set_aggregates_for_provider')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_get_provider_aggregates')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_provider_by_name')
    def test_aggregate_add_host_retry_success(
            self, mock_get_by_name, mock_get_aggs, mock_set_aggs):
        mock_get_by_name.return_value = {
            'uuid': uuids.cn1,
            'generation': 1,
        }
        gens = (42, 43, 44)
        mock_get_aggs.side_effect = (
            report.AggInfo(aggregates=set([]), generation=gen) for gen in gens)
        mock_set_aggs.side_effect = (
            exception.ResourceProviderUpdateConflict(
                uuid='uuid', generation=42, error='error'),
            exception.ResourceProviderUpdateConflict(
                uuid='uuid', generation=43, error='error'),
            None,
        )
        self.client.aggregate_add_host(self.context, uuids.agg1,
                                       host_name='cn1')
        mock_set_aggs.assert_has_calls([mock.call(
            self.context, uuids.cn1, set([uuids.agg1]), use_cache=False,
            generation=gen) for gen in gens])

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'set_aggregates_for_provider')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_get_provider_aggregates')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_provider_by_name')
    def test_aggregate_add_host_retry_raises(
            self, mock_get_by_name, mock_get_aggs, mock_set_aggs):
        mock_get_by_name.return_value = {
            'uuid': uuids.cn1,
            'generation': 1,
        }
        gens = (42, 43, 44, 45)
        mock_get_aggs.side_effect = (
            report.AggInfo(aggregates=set([]), generation=gen) for gen in gens)
        mock_set_aggs.side_effect = (
            exception.ResourceProviderUpdateConflict(
                uuid='uuid', generation=gen, error='error') for gen in gens)
        self.assertRaises(
            exception.ResourceProviderUpdateConflict,
            self.client.aggregate_add_host, self.context, uuids.agg1,
            host_name='cn1')
        mock_set_aggs.assert_has_calls([mock.call(
            self.context, uuids.cn1, set([uuids.agg1]), use_cache=False,
            generation=gen) for gen in gens])

    def test_aggregate_add_host_no_host_name_or_rp_uuid(self):
        self.assertRaises(
            ValueError,
            self.client.aggregate_add_host, self.context, uuids.agg1)

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_provider_by_name',
                side_effect=exception.PlacementAPIConnectFailure)
    def test_aggregate_remove_host_no_placement(self, mock_get_by_name):
        """Tests that PlacementAPIConnectFailure will be raised up from
        aggregate_remove_host if get_provider_by_name raises that error.
        """
        name = 'cn1'
        agg_uuid = uuids.agg1
        self.assertRaises(
            exception.PlacementAPIConnectFailure,
            self.client.aggregate_remove_host, self.context, agg_uuid, name)
        self.mock_get.assert_not_called()

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'set_aggregates_for_provider')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_get_provider_aggregates')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_provider_by_name')
    def test_aggregate_remove_host_success_already_existing(
            self, mock_get_by_name, mock_get_aggs, mock_set_aggs):
        mock_get_by_name.return_value = {
            'uuid': uuids.cn1,
            'generation': 1,
        }
        agg_uuid = uuids.agg1
        mock_get_aggs.return_value = report.AggInfo(aggregates=set([agg_uuid]),
                                                    generation=42)
        name = 'cn1'
        self.client.aggregate_remove_host(self.context, agg_uuid, name)
        mock_set_aggs.assert_called_once_with(
            self.context, uuids.cn1, set([]), use_cache=False, generation=42)

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'set_aggregates_for_provider')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_get_provider_aggregates')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_provider_by_name')
    def test_aggregate_remove_host_success_no_existing(
            self, mock_get_by_name, mock_get_aggs, mock_set_aggs):
        mock_get_by_name.return_value = {
            'uuid': uuids.cn1,
            'generation': 1,
        }
        agg1_uuid = uuids.agg1
        agg2_uuid = uuids.agg2
        agg3_uuid = uuids.agg3
        mock_get_aggs.return_value = report.AggInfo(aggregates=set([]),
                                                    generation=42)
        name = 'cn1'
        self.client.aggregate_remove_host(self.context, agg2_uuid, name)
        mock_set_aggs.assert_not_called()
        mock_get_aggs.reset_mock()
        mock_set_aggs.reset_mock()
        mock_get_aggs.return_value = report.AggInfo(
            aggregates=set([agg1_uuid, agg2_uuid, agg3_uuid]), generation=43)
        self.client.aggregate_remove_host(self.context, agg2_uuid, name)
        mock_set_aggs.assert_called_once_with(
            self.context, uuids.cn1, set([agg1_uuid, agg3_uuid]),
            use_cache=False, generation=43)

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'set_aggregates_for_provider')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_get_provider_aggregates')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_provider_by_name')
    def test_aggregate_remove_host_retry_success(
            self, mock_get_by_name, mock_get_aggs, mock_set_aggs):
        mock_get_by_name.return_value = {
            'uuid': uuids.cn1,
            'generation': 1,
        }
        gens = (42, 43, 44)
        mock_get_aggs.side_effect = (
            report.AggInfo(aggregates=set([uuids.agg1]), generation=gen)
            for gen in gens)
        mock_set_aggs.side_effect = (
            exception.ResourceProviderUpdateConflict(
                uuid='uuid', generation=42, error='error'),
            exception.ResourceProviderUpdateConflict(
                uuid='uuid', generation=43, error='error'),
            None,
        )
        self.client.aggregate_remove_host(self.context, uuids.agg1, 'cn1')
        mock_set_aggs.assert_has_calls([mock.call(
            self.context, uuids.cn1, set([]), use_cache=False,
            generation=gen) for gen in gens])

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'set_aggregates_for_provider')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                '_get_provider_aggregates')
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
                'get_provider_by_name')
    def test_aggregate_remove_host_retry_raises(
            self, mock_get_by_name, mock_get_aggs, mock_set_aggs):
        mock_get_by_name.return_value = {
            'uuid': uuids.cn1,
            'generation': 1,
        }
        gens = (42, 43, 44, 45)
        mock_get_aggs.side_effect = (
            report.AggInfo(aggregates=set([uuids.agg1]), generation=gen)
            for gen in gens)
        mock_set_aggs.side_effect = (
            exception.ResourceProviderUpdateConflict(
                uuid='uuid', generation=gen, error='error') for gen in gens)
        self.assertRaises(
            exception.ResourceProviderUpdateConflict,
            self.client.aggregate_remove_host, self.context, uuids.agg1, 'cn1')
        mock_set_aggs.assert_has_calls([mock.call(
            self.context, uuids.cn1, set([]), use_cache=False,
            generation=gen) for gen in gens])


class TestUsages(SchedulerReportClientTestCase):
    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.get')
    def test_get_usages_counts_for_quota_fail(self, mock_get):
        # First call with project fails
        mock_get.return_value = fake_requests.FakeResponse(500, content='err')
        self.assertRaises(exception.UsagesRetrievalFailed,
                          self.client.get_usages_counts_for_quota,
                          self.context, 'fake-project')
        mock_get.assert_called_once_with(
            '/usages?project_id=fake-project', version='1.9',
            global_request_id=self.context.global_id)
        # Second call with project + user fails
        mock_get.reset_mock()
        fake_good_response = fake_requests.FakeResponse(
            200, content=jsonutils.dumps(
                {'usages': {orc.VCPU: 2,
                            orc.MEMORY_MB: 512}}))
        mock_get.side_effect = [fake_good_response,
                                fake_requests.FakeResponse(500, content='err')]
        self.assertRaises(exception.UsagesRetrievalFailed,
                          self.client.get_usages_counts_for_quota,
                          self.context, 'fake-project', user_id='fake-user')
        self.assertEqual(2, mock_get.call_count)
        call1 = mock.call(
            '/usages?project_id=fake-project', version='1.9',
            global_request_id=self.context.global_id)
        call2 = mock.call(
            '/usages?project_id=fake-project&user_id=fake-user', version='1.9',
            global_request_id=self.context.global_id)
        mock_get.assert_has_calls([call1, call2])

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.get')
    def test_get_usages_counts_for_quota_retries(self, mock_get):
        # Two attempts have a ConnectFailure and the third succeeds
        fake_project_response = fake_requests.FakeResponse(
            200, content=jsonutils.dumps(
                {'usages': {orc.VCPU: 2,
                            orc.MEMORY_MB: 512}}))
        mock_get.side_effect = [ks_exc.ConnectFailure,
                                ks_exc.ConnectFailure,
                                fake_project_response]
        counts = self.client.get_usages_counts_for_quota(self.context,
                                                         'fake-project')
        self.assertEqual(3, mock_get.call_count)
        expected = {'project': {'cores': 2, 'ram': 512}}
        self.assertDictEqual(expected, counts)

        # Project query succeeds, first project + user query has a
        # ConnectFailure, second project + user query succeeds
        mock_get.reset_mock()
        fake_user_response = fake_requests.FakeResponse(
            200, content=jsonutils.dumps(
                {'usages': {orc.VCPU: 1,
                            orc.MEMORY_MB: 256}}))
        mock_get.side_effect = [fake_project_response,
                                ks_exc.ConnectFailure,
                                fake_user_response]
        counts = self.client.get_usages_counts_for_quota(
            self.context, 'fake-project', user_id='fake-user')
        self.assertEqual(3, mock_get.call_count)
        expected['user'] = {'cores': 1, 'ram': 256}
        self.assertDictEqual(expected, counts)

        # Three attempts in a row have a ConnectFailure
        mock_get.reset_mock()
        mock_get.side_effect = [ks_exc.ConnectFailure] * 4
        self.assertRaises(ks_exc.ConnectFailure,
                          self.client.get_usages_counts_for_quota,
                          self.context, 'fake-project')

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.get')
    def test_get_usages_counts_default_zero(self, mock_get):
        # A project and user are not yet consuming any resources.
        fake_response = fake_requests.FakeResponse(
            200, content=jsonutils.dumps({'usages': {}}))
        mock_get.side_effect = [fake_response, fake_response]

        counts = self.client.get_usages_counts_for_quota(
            self.context, 'fake-project', user_id='fake-user')

        self.assertEqual(2, mock_get.call_count)
        expected = {'project': {'cores': 0, 'ram': 0},
                    'user': {'cores': 0, 'ram': 0}}
        self.assertDictEqual(expected, counts)

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.get')
    def test_get_usages_count_with_pcpu(self, mock_get):
        fake_responses = fake_requests.FakeResponse(
            200,
            content=jsonutils.dumps({'usages': {orc.VCPU: 2, orc.PCPU: 2}}))
        mock_get.return_value = fake_responses
        counts = self.client.get_usages_counts_for_quota(
            self.context, 'fake-project', user_id='fake-user')
        self.assertEqual(2, mock_get.call_count)
        expected = {'project': {'cores': 4, 'ram': 0},
                    'user': {'cores': 4, 'ram': 0}}
        self.assertDictEqual(expected, counts)

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.get')
    def test_get_usages_counts_for_limits(self, mock_get):
        fake_responses = fake_requests.FakeResponse(
            200,
            content=jsonutils.dumps({'usages': {orc.VCPU: 2, orc.PCPU: 2}}))
        mock_get.return_value = fake_responses

        counts = self.client.get_usages_counts_for_limits(
            self.context, 'fake-project')

        expected = {orc.VCPU: 2, orc.PCPU: 2}
        self.assertDictEqual(expected, counts)
        self.assertEqual(1, mock_get.call_count)

    @mock.patch('nova.scheduler.client.report.SchedulerReportClient.get')
    def test_get_usages_counts_for_limits_fails(self, mock_get):
        fake_failure_response = fake_requests.FakeResponse(500)
        mock_get.side_effect = [ks_exc.ConnectFailure, fake_failure_response]

        e = self.assertRaises(exception.UsagesRetrievalFailed,
                              self.client.get_usages_counts_for_limits,
                              self.context, 'fake-project')

        expected = "Failed to retrieve usages for project 'fake-project' " \
                   "and user 'N/A'."
        self.assertEqual(expected, str(e))
        self.assertEqual(2, mock_get.call_count)
