nova/nova/tests/unit/scheduler/client/test_report.py

4609 lines
186 KiB
Python

# 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 time
from urllib import parse
import fixtures
from keystoneauth1 import exceptions as ks_exc
import mock
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()
with mock.patch('keystoneauth1.loading.load_auth_from_conf_options'):
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)
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.35',
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.35',
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.35',
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):
resp_mock = mock.Mock(status_code=200)
rpjson = [
{
'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': [],
},
]
resp_mock.json.return_value = {'resource_providers': rpjson}
self.ks_adap_mock.get.return_value = resp_mock
result = self.client._get_sharing_providers(
self.context, [uuids.agg1, uuids.agg2])
expected_url = ('/resource_providers?member_of=in:' +
','.join((uuids.agg1, uuids.agg2)) +
'&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)
self.assertEqual(rpjson, 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.object(report.LOG, 'debug')
def test_refresh_associations_time(self, log_mock):
"""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 = time.time()
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()
with mock.patch('time.time') as mock_future:
# Not called a second time because not enough time has passed.
mock_future.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_future.return_value = (now +
CONF.compute.resource_provider_association_refresh + 1)
self.client._refresh_associations(self.context, uuid)
self.assert_getters_were_called(uuid)
def test_refresh_associations_disabled(self):
"""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 = time.time()
self.client._refresh_associations(self.context, uuid)
self.assert_getters_were_called(uuid)
# Clear call count.
self.reset_getter_mocks()
with mock.patch('time.time') as mock_future:
# A lot of time passes
mock_future.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_future.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_future.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.'
'get_providers_in_tree')
@mock.patch("nova.scheduler.client.report.SchedulerReportClient."
"delete")
@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_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)
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."
"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_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()
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)