From e041fddeb027f909547ec46cb198262d410f7df7 Mon Sep 17 00:00:00 2001 From: Jay Pipes Date: Thu, 13 Jul 2017 15:29:15 -0400 Subject: [PATCH] add dict of allocation requests to select_dests() For scheduler drivers that use allocation candidates, creates a dict, keyed by resource provider UUID, of lists of allocation request JSON objects that may be used to attempt resource claims for hosts selected by the driver. This patch prepares the foundation for those resource claims by simply reworking the calling interface of the driver select_destinations() method to accept this dict of allocation request lists. Change-Id: Icaa5d44bb52894f509b95f4d8e69aab8bd2b31f2 blueprint: placement-claims --- nova/scheduler/chance.py | 2 +- nova/scheduler/filter_scheduler.py | 59 +++++++++++++++++-- nova/scheduler/manager.py | 40 +++++++++---- nova/tests/unit/scheduler/fakes.py | 17 ++++++ .../unit/scheduler/test_caching_scheduler.py | 17 +++--- .../unit/scheduler/test_chance_scheduler.py | 4 +- .../unit/scheduler/test_filter_scheduler.py | 19 +++--- nova/tests/unit/scheduler/test_scheduler.py | 32 +++++++--- 8 files changed, 145 insertions(+), 45 deletions(-) diff --git a/nova/scheduler/chance.py b/nova/scheduler/chance.py index 827049354cbe..3bcb244c8bc7 100644 --- a/nova/scheduler/chance.py +++ b/nova/scheduler/chance.py @@ -58,7 +58,7 @@ class ChanceScheduler(driver.Scheduler): return random.choice(hosts) def select_destinations(self, context, spec_obj, instance_uuids, - provider_summaries): + alloc_reqs_by_rp_uuid, provider_summaries): """Selects random destinations.""" num_instances = spec_obj.num_instances # NOTE(timello): Returns a list of dicts with 'host', 'nodename' and diff --git a/nova/scheduler/filter_scheduler.py b/nova/scheduler/filter_scheduler.py index a773e6ecf74e..3bdac14c58a5 100644 --- a/nova/scheduler/filter_scheduler.py +++ b/nova/scheduler/filter_scheduler.py @@ -41,9 +41,33 @@ class FilterScheduler(driver.Scheduler): self.notifier = rpc.get_notifier('scheduler') def select_destinations(self, context, spec_obj, instance_uuids, - provider_summaries): + alloc_reqs_by_rp_uuid, provider_summaries): """Returns a sorted list of HostState objects that satisfy the supplied request_spec. + + :param context: The RequestContext object + :param spec_obj: The RequestSpec object + :param instance_uuids: List of UUIDs, one for each value of the spec + object's num_instances attribute + :param alloc_reqs_by_rp_uuid: Optional dict, keyed by resource provider + UUID, of the allocation requests that may + be used to claim resources against + matched hosts. If None, indicates either + the placement API wasn't reachable or + that there were no allocation requests + returned by the placement API. If the + latter, the provider_summaries will be an + empty dict, not None. + :param provider_summaries: Optional dict, keyed by resource provider + UUID, of information that will be used by + the filters/weighers in selecting matching + hosts for a request. If None, indicates that + the scheduler driver should grab all compute + node information locally and that the + Placement API is not used. If an empty dict, + indicates the Placement API returned no + potential matches for the requested + resources. """ self.notifier.info( context, 'scheduler.select_destinations.start', @@ -51,7 +75,7 @@ class FilterScheduler(driver.Scheduler): num_instances = spec_obj.num_instances selected_hosts = self._schedule(context, spec_obj, instance_uuids, - provider_summaries) + alloc_reqs_by_rp_uuid, provider_summaries) # Couldn't fulfill the request_spec if len(selected_hosts) < num_instances: @@ -79,9 +103,34 @@ class FilterScheduler(driver.Scheduler): dict(request_spec=spec_obj.to_legacy_request_spec_dict())) return selected_hosts - def _schedule(self, context, spec_obj, instance_uuids, provider_summaries): - """Returns a list of hosts that meet the required specs, - ordered by their fitness. + def _schedule(self, context, spec_obj, instance_uuids, + alloc_reqs_by_rp_uuid, provider_summaries): + """Returns a list of hosts that meet the required specs, ordered by + their fitness. + + :param context: The RequestContext object + :param spec_obj: The RequestSpec object + :param instance_uuids: List of UUIDs, one for each value of the spec + object's num_instances attribute + :param alloc_reqs_by_rp_uuid: Optional dict, keyed by resource provider + UUID, of the allocation requests that may + be used to claim resources against + matched hosts. If None, indicates either + the placement API wasn't reachable or + that there were no allocation requests + returned by the placement API. If the + latter, the provider_summaries will be an + empty dict, not None. + :param provider_summaries: Optional dict, keyed by resource provider + UUID, of information that will be used by + the filters/weighers in selecting matching + hosts for a request. If None, indicates that + the scheduler driver should grab all compute + node information locally and that the + Placement API is not used. If an empty dict, + indicates the Placement API returned no + potential matches for the requested + resources. """ elevated = context.elevated() diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py index 98116b92f92a..e1c83cbc9e1d 100644 --- a/nova/scheduler/manager.py +++ b/nova/scheduler/manager.py @@ -19,6 +19,8 @@ Scheduler Service """ +import collections + from oslo_log import log as logging import oslo_messaging as messaging from oslo_serialization import jsonutils @@ -108,28 +110,40 @@ class SchedulerManager(manager.Manager): request_spec, filter_properties) resources = utils.resources_from_request_spec(spec_obj) - alloc_reqs, p_sums = None, None + alloc_reqs_by_rp_uuid, provider_summaries = None, None if self.driver.USES_ALLOCATION_CANDIDATES: res = self.placement_client.get_allocation_candidates(resources) - # We have to handle the case that we failed to connect to the - # Placement service and the safe_connect decorator on - # get_allocation_candidates returns None. - alloc_reqs, p_sums = res if res is not None else (None, None) + if res is None: + # We have to handle the case that we failed to connect to the + # Placement service and the safe_connect decorator on + # get_allocation_candidates returns None. + alloc_reqs, provider_summaries = None, None + else: + alloc_reqs, provider_summaries = res if not alloc_reqs: LOG.debug("Got no allocation candidates from the Placement " "API. This may be a temporary occurrence as compute " "nodes start up and begin reporting inventory to " "the Placement service.") - # TODO(jaypipes): Setting p_sums to None triggers the scheduler - # to load all compute nodes to do scheduling "the old way". - # Really, we should raise NoValidHosts here, but all functional - # tests will fall over if we do that without changing the - # PlacementFixture to load compute node inventory into the - # placement database before starting functional tests. - p_sums = None + # TODO(jaypipes): Setting provider_summaries to None triggers + # the scheduler to load all compute nodes to do scheduling "the + # old way". Really, we should raise NoValidHosts here, but all + # functional tests will fall over if we do that without + # changing the PlacementFixture to load compute node inventory + # into the placement database before starting functional tests. + provider_summaries = None + else: + # Build a dict of lists of allocation requests, keyed by + # provider UUID, so that when we attempt to claim resources for + # a host, we can grab an allocation request easily + alloc_reqs_by_rp_uuid = collections.defaultdict(list) + for ar in alloc_reqs: + for rr in ar['allocations']: + rp_uuid = rr['resource_provider']['uuid'] + alloc_reqs_by_rp_uuid[rp_uuid].append(ar) dests = self.driver.select_destinations(ctxt, spec_obj, instance_uuids, - p_sums) + alloc_reqs_by_rp_uuid, provider_summaries) dest_dicts = [_host_state_obj_to_dict(d) for d in dests] return jsonutils.to_primitive(dest_dicts) diff --git a/nova/tests/unit/scheduler/fakes.py b/nova/tests/unit/scheduler/fakes.py index 5d24629ea9d4..752ce59de116 100644 --- a/nova/tests/unit/scheduler/fakes.py +++ b/nova/tests/unit/scheduler/fakes.py @@ -127,6 +127,23 @@ COMPUTE_NODES = [ host='fake', hypervisor_hostname='fake-hyp'), ] +ALLOC_REQS = [ + { + 'allocations': [ + { + 'resource_provider': { + 'uuid': cn.uuid, + }, + 'resources': { + 'VCPU': 1, + 'MEMORY_MB': 512, + 'DISK_GB': 512, + }, + }, + ] + } for cn in COMPUTE_NODES +] + RESOURCE_PROVIDERS = [ dict( uuid=uuidsentinel.rp1, diff --git a/nova/tests/unit/scheduler/test_caching_scheduler.py b/nova/tests/unit/scheduler/test_caching_scheduler.py index 1be8a61b4d37..21a9a59160de 100644 --- a/nova/tests/unit/scheduler/test_caching_scheduler.py +++ b/nova/tests/unit/scheduler/test_caching_scheduler.py @@ -87,7 +87,7 @@ class CachingSchedulerTestCase(test_scheduler.SchedulerTestCase): self.assertRaises(exception.NoValidHost, self.driver.select_destinations, self.context, spec_obj, [spec_obj.instance_uuid], - {}) + {}, {}) @mock.patch('nova.db.instance_extra_get_by_instance_uuid', return_value={'numa_topology': None, @@ -103,13 +103,14 @@ class CachingSchedulerTestCase(test_scheduler.SchedulerTestCase): self.assertEqual(result[0].host, fake_host.host) def _test_select_destinations(self, spec_obj): - p_sums = {} + provider_summaries = {} for cell_hosts in self.driver.all_host_states.values(): for hs in cell_hosts: - p_sums[hs.uuid] = hs + provider_summaries[hs.uuid] = hs return self.driver.select_destinations( - self.context, spec_obj, [spec_obj.instance_uuid], p_sums) + self.context, spec_obj, [spec_obj.instance_uuid], {}, + provider_summaries) def _get_fake_request_spec(self): # NOTE(sbauza): Prevent to stub the Flavor.get_by_id call just by @@ -179,14 +180,14 @@ class CachingSchedulerTestCase(test_scheduler.SchedulerTestCase): host_state = self._get_fake_host_state(x) host_states.append(host_state) self.driver.all_host_states = {uuids.cell: host_states} - p_sums = {hs.uuid: hs for hs in host_states} + provider_summaries = {hs.uuid: hs for hs in host_states} def run_test(): a = timeutils.utcnow() for x in range(requests): self.driver.select_destinations(self.context, spec_obj, - [spec_obj.instance_uuid], p_sums) + [spec_obj.instance_uuid], {}, provider_summaries) b = timeutils.utcnow() c = b - a @@ -232,12 +233,12 @@ class CachingSchedulerTestCase(test_scheduler.SchedulerTestCase): uuids.cell1: host_states_cell1, uuids.cell2: host_states_cell2, } - p_sums = { + provider_summaries = { cn.uuid: cn for cn in host_states_cell1 + host_states_cell2 } d = self.driver.select_destinations(self.context, spec_obj, - [spec_obj.instance_uuid], p_sums) + [spec_obj.instance_uuid], {}, provider_summaries) self.assertIn(d[0].host, [hs.host for hs in host_states_cell2]) diff --git a/nova/tests/unit/scheduler/test_chance_scheduler.py b/nova/tests/unit/scheduler/test_chance_scheduler.py index 38fa233eb222..2d60a7dd312a 100644 --- a/nova/tests/unit/scheduler/test_chance_scheduler.py +++ b/nova/tests/unit/scheduler/test_chance_scheduler.py @@ -64,7 +64,7 @@ class ChanceSchedulerTestCase(test_scheduler.SchedulerTestCase): spec_obj = objects.RequestSpec(num_instances=2, ignore_hosts=None) dests = self.driver.select_destinations(self.context, spec_obj, - [uuids.instance1, uuids.instance2], + [uuids.instance1, uuids.instance2], {}, mock.sentinel.p_sums) self.assertEqual(2, len(dests)) @@ -95,5 +95,5 @@ class ChanceSchedulerTestCase(test_scheduler.SchedulerTestCase): spec_obj.instance_uuid = uuids.instance self.assertRaises(exception.NoValidHost, self.driver.select_destinations, self.context, - spec_obj, [spec_obj.instance_uuid], + spec_obj, [spec_obj.instance_uuid], {}, mock.sentinel.p_sums) diff --git a/nova/tests/unit/scheduler/test_filter_scheduler.py b/nova/tests/unit/scheduler/test_filter_scheduler.py index a1912819b594..2dcec4abeec9 100644 --- a/nova/tests/unit/scheduler/test_filter_scheduler.py +++ b/nova/tests/unit/scheduler/test_filter_scheduler.py @@ -57,7 +57,8 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): instance_uuids = [uuids.instance] ctx = mock.Mock() selected_hosts = self.driver._schedule(ctx, spec_obj, - instance_uuids, mock.sentinel.provider_summaries) + instance_uuids, mock.sentinel.alloc_reqs_by_rp_uuid, + mock.sentinel.provider_summaries) mock_get_all_states.assert_called_once_with( ctx.elevated.return_value, spec_obj, @@ -105,8 +106,9 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): getattr(uuids, 'instance%d' % x) for x in range(num_instances) ] ctx = mock.Mock() - self.driver._schedule(ctx, spec_obj, - instance_uuids, mock.sentinel.provider_summaries) + self.driver._schedule(ctx, spec_obj, instance_uuids, + mock.sentinel.alloc_reqs_by_rp_uuid, + mock.sentinel.provider_summaries) # Check that _get_sorted_hosts() is called twice and that the # second time, we pass it the hosts that were returned from @@ -262,10 +264,12 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): mock_schedule.return_value = [mock.sentinel.hs1] dests = self.driver.select_destinations(self.context, spec_obj, - mock.sentinel.instance_uuids, mock.sentinel.p_sums) + mock.sentinel.instance_uuids, mock.sentinel.alloc_reqs_by_rp_uuid, + mock.sentinel.p_sums) mock_schedule.assert_called_once_with(self.context, spec_obj, - mock.sentinel.instance_uuids, mock.sentinel.p_sums) + mock.sentinel.instance_uuids, mock.sentinel.alloc_reqs_by_rp_uuid, + mock.sentinel.p_sums) self.assertEqual([mock.sentinel.hs1], dests) @@ -290,7 +294,8 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): self.assertRaises(exception.NoValidHost, self.driver.select_destinations, self.context, spec_obj, - mock.sentinel.instance_uuids, mock.sentinel.p_sums) + mock.sentinel.instance_uuids, mock.sentinel.alloc_reqs_by_rp_uuid, + mock.sentinel.p_sums) # Verify that the host state object has been marked as not updated so # it's picked up in the next pull from the DB for compute node objects @@ -309,7 +314,7 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): instance_uuid=uuids.instance) self.driver.select_destinations(self.context, spec_obj, - [uuids.instance], {}) + [uuids.instance], {}, None) expected = [ mock.call(self.context, 'scheduler.select_destinations.start', diff --git a/nova/tests/unit/scheduler/test_scheduler.py b/nova/tests/unit/scheduler/test_scheduler.py index 07dfe1772d08..e0a7eea8fee0 100644 --- a/nova/tests/unit/scheduler/test_scheduler.py +++ b/nova/tests/unit/scheduler/test_scheduler.py @@ -96,14 +96,19 @@ class SchedulerManagerTestCase(test.NoDBTestCase): def test_select_destination(self, mock_get_ac, mock_rfrs): fake_spec = objects.RequestSpec() fake_spec.instance_uuid = uuids.instance - place_res = (mock.sentinel.alloc_reqs, mock.sentinel.p_sums) + place_res = (fakes.ALLOC_REQS, mock.sentinel.p_sums) mock_get_ac.return_value = place_res + expected_alloc_reqs_by_rp_uuid = { + cn.uuid: [fakes.ALLOC_REQS[x]] + for x, cn in enumerate(fakes.COMPUTE_NODES) + } with mock.patch.object(self.manager.driver, 'select_destinations' ) as select_destinations: self.manager.select_destinations(None, spec_obj=fake_spec, instance_uuids=[fake_spec.instance_uuid]) select_destinations.assert_called_once_with(None, fake_spec, - [fake_spec.instance_uuid], mock.sentinel.p_sums) + [fake_spec.instance_uuid], expected_alloc_reqs_by_rp_uuid, + mock.sentinel.p_sums) mock_get_ac.assert_called_once_with(mock_rfrs.return_value) @mock.patch('nova.scheduler.utils.resources_from_request_spec') @@ -124,7 +129,7 @@ class SchedulerManagerTestCase(test.NoDBTestCase): self.manager.select_destinations(None, spec_obj=fake_spec, instance_uuids=[fake_spec.instance_uuid]) select_destinations.assert_called_once_with(None, fake_spec, - [fake_spec.instance_uuid], None) + [fake_spec.instance_uuid], None, None) mock_get_ac.assert_called_once_with(mock_rfrs.return_value) @mock.patch('nova.scheduler.utils.resources_from_request_spec') @@ -146,7 +151,7 @@ class SchedulerManagerTestCase(test.NoDBTestCase): self.context, spec_obj=fake_spec, instance_uuids=[fake_spec.instance_uuid]) select_destinations.assert_called_once_with( - self.context, fake_spec, [fake_spec.instance_uuid], None) + self.context, fake_spec, [fake_spec.instance_uuid], None, None) mock_get_ac.assert_called_once_with(mock_rfrs.return_value) @mock.patch('nova.scheduler.utils.resources_from_request_spec') @@ -168,7 +173,7 @@ class SchedulerManagerTestCase(test.NoDBTestCase): self.manager.select_destinations(None, spec_obj=fake_spec, instance_uuids=[fake_spec.instance_uuid]) select_destinations.assert_called_once_with(None, fake_spec, - [fake_spec.instance_uuid], None) + [fake_spec.instance_uuid], None, None) mock_get_ac.assert_called_once_with(mock_rfrs.return_value) @mock.patch('nova.scheduler.utils.resources_from_request_spec') @@ -176,13 +181,17 @@ class SchedulerManagerTestCase(test.NoDBTestCase): 'get_allocation_candidates') def test_select_destination_with_4_3_client(self, mock_get_ac, mock_rfrs): fake_spec = objects.RequestSpec() - place_res = (mock.sentinel.alloc_reqs, mock.sentinel.p_sums) + place_res = (fakes.ALLOC_REQS, mock.sentinel.p_sums) mock_get_ac.return_value = place_res + expected_alloc_reqs_by_rp_uuid = { + cn.uuid: [fakes.ALLOC_REQS[x]] + for x, cn in enumerate(fakes.COMPUTE_NODES) + } with mock.patch.object(self.manager.driver, 'select_destinations' ) as select_destinations: self.manager.select_destinations(None, spec_obj=fake_spec) select_destinations.assert_called_once_with(None, fake_spec, None, - mock.sentinel.p_sums) + expected_alloc_reqs_by_rp_uuid, mock.sentinel.p_sums) mock_get_ac.assert_called_once_with(mock_rfrs.return_value) # TODO(sbauza): Remove that test once the API v4 is removed @@ -195,15 +204,20 @@ class SchedulerManagerTestCase(test.NoDBTestCase): fake_spec = objects.RequestSpec() fake_spec.instance_uuid = uuids.instance from_primitives.return_value = fake_spec - place_res = (mock.sentinel.alloc_reqs, mock.sentinel.p_sums) + place_res = (fakes.ALLOC_REQS, mock.sentinel.p_sums) mock_get_ac.return_value = place_res + expected_alloc_reqs_by_rp_uuid = { + cn.uuid: [fakes.ALLOC_REQS[x]] + for x, cn in enumerate(fakes.COMPUTE_NODES) + } with mock.patch.object(self.manager.driver, 'select_destinations' ) as select_destinations: self.manager.select_destinations(None, request_spec='fake_spec', filter_properties='fake_props', instance_uuids=[fake_spec.instance_uuid]) select_destinations.assert_called_once_with(None, fake_spec, - [fake_spec.instance_uuid], mock.sentinel.p_sums) + [fake_spec.instance_uuid], expected_alloc_reqs_by_rp_uuid, + mock.sentinel.p_sums) mock_get_ac.assert_called_once_with(mock_rfrs.return_value) def test_update_aggregates(self):