Report client: get_allocations_for_provider_tree

The reshaper path needs to pass all the allocations related to the
compute node's provider tree to update_provider_tree so it can shuffle
those allocations appropriately. This patch adds a new
get_allocations_for_provider_tree method to the report client for this
purpose.

Blueprint: reshape-provider-tree

Change-Id: I73811f3e3bf19dec3a240e1f1f8c69f4c98d677c
This commit is contained in:
Eric Fried 2018-07-22 08:06:34 -05:00
parent 176d1d90fd
commit 25b852efd7
2 changed files with 194 additions and 0 deletions

View File

@ -1997,6 +1997,75 @@ class SchedulerReportClient(object):
data = resp.json()
return ProviderAllocInfo(allocations=data['allocations'])
def get_allocations_for_provider_tree(self, context, nodename):
"""Retrieve allocation records associated with all providers in the
provider tree.
This method uses the cache exclusively to discover providers. The
caller must ensure that the cache is populated.
This method is (and should remain) used exclusively in the reshaper
flow by the resource tracker.
:param context: The security context
:param nodename: The name of a node for whose tree we are getting
allocations.
:returns: A dict, keyed by consumer UUID, of allocation records:
{ $CONSUMER_UUID: {
# The shape of each "allocations" dict below is identical
# to the return from GET /allocations/{consumer_uuid}
"allocations": {
$RP_UUID: {
"generation": $RP_GEN,
"resources": {
$RESOURCE_CLASS: $AMOUNT,
...
},
},
...
},
"project_id": $PROJ_ID,
"user_id": $USER_ID,
"consumer_generation": $CONSUMER_GEN,
},
...
}
:raises: keystoneauth1.exceptions.ClientException if placement API
communication fails.
:raises: ResourceProviderAllocationRetrievalFailed if a placement API
call fails.
:raises: ValueError if there's no provider with the specified nodename.
"""
# NOTE(efried): Despite our best efforts, there are some scenarios
# (e.g. mid-evacuate) where we can still wind up returning allocations
# against providers belonging to other hosts. We count on the consumer
# of this information (i.e. the reshaper flow of a virt driver's
# update_provider_tree) to ignore allocations associated with any
# provider it is not reshaping - and it should never be reshaping
# providers belonging to other hosts.
# We can't get *all* allocations for associated sharing providers
# because some of those will belong to consumers on other hosts. So we
# have to discover all the consumers associated with the providers in
# the "local" tree (we use the nodename to figure out which providers
# are "local").
# All we want to do at this point is accumulate the set of consumers we
# care about.
consumers = set()
# TODO(efried): This could be more efficient if placement offered an
# operation like GET /allocations?rp_uuid=in:<list>
for u in self._provider_tree.get_provider_uuids(name_or_uuid=nodename):
alloc_info = self.get_allocations_for_resource_provider(context, u)
# The allocations dict is keyed by consumer UUID
consumers.update(alloc_info.allocations)
# Now get all the allocations (which will include allocations on
# sharing providers) for each of these consumers to build the result.
# TODO(efried): This could be more efficient if placement offered an
# operation like GET /allocations?consumer_uuid=in:<list>
return {consumer: self.get_allocs_for_consumer(context, consumer)
for consumer in consumers}
def delete_resource_provider(self, context, compute_node, cascade=False):
"""Deletes the ResourceProvider record for the compute_node.

View File

@ -1032,3 +1032,128 @@ class SchedulerReportClientTests(SchedulerReportClientTestBase):
with self._interceptor():
self.client.get_allocation_candidates(
self.context, utils.ResourceRequest())
def test_get_allocations_for_provider_tree(self):
with self._interceptor():
# When the provider tree cache is empty (or we otherwise supply a
# bogus node name), we get ValueError.
self.assertRaises(ValueError,
self.client.get_allocations_for_provider_tree,
self.context, 'bogus')
# get_provider_tree_and_ensure_root creates a resource provider
# record for us
ptree = self.client.get_provider_tree_and_ensure_root(
self.context, self.compute_uuid, name=self.compute_name)
ptree.update_inventory(self.compute_uuid,
{'MEMORY_MB': {'total': 2048}})
ptree.update_aggregates(self.compute_uuid, [uuids.agg1])
# These are part of the compute node's tree
ptree.new_child('numa1', self.compute_uuid, uuid=uuids.numa1)
ptree.update_inventory('numa1', {'VCPU': {'total': 8},
'CUSTOM_PCPU': {'total': 8}})
ptree.new_child('numa2', self.compute_uuid, uuid=uuids.numa2)
ptree.update_inventory('numa2', {'VCPU': {'total': 8},
'CUSTOM_PCPU': {'total': 8}})
# A sharing provider that's not part of the compute node's tree.
# We avoid the report client's convenience methods to get bonus
# coverage of the subsequent update_from_provider_tree pulling it
# into the cache for us.
resp = self.client.post(
'/resource_providers',
{'uuid': uuids.ssp, 'name': 'ssp'}, version='1.20')
resp = self.client.put(
'/resource_providers/%s/inventories' % uuids.ssp,
{'inventories': {'DISK_GB': {'total': 500}},
'resource_provider_generation': resp.json()['generation']})
# Part of the shared storage aggregate
resp = self.client.put(
'/resource_providers/%s/aggregates' % uuids.ssp,
{'aggregates': [uuids.agg1],
'resource_provider_generation':
resp.json()['resource_provider_generation']},
version='1.19')
self.client.put(
'/resource_providers/%s/traits' % uuids.ssp,
{'traits': ['MISC_SHARES_VIA_AGGREGATE'],
'resource_provider_generation':
resp.json()['resource_provider_generation']})
self.client.update_from_provider_tree(self.context, ptree)
# Another unrelated compute node. We don't use the report client's
# convenience methods because we don't want this guy in the cache.
resp = self.client.post(
'/resource_providers',
{'uuid': uuids.othercn, 'name': 'othercn'}, version='1.20')
resp = self.client.put(
'/resource_providers/%s/inventories' % uuids.othercn,
{'inventories': {'VCPU': {'total': 8},
'MEMORY_MB': {'total': 1024}},
'resource_provider_generation': resp.json()['generation']})
# Part of the shared storage aggregate
self.client.put(
'/resource_providers/%s/aggregates' % uuids.othercn,
{'aggregates': [uuids.agg1],
'resource_provider_generation':
resp.json()['resource_provider_generation']},
version='1.19')
# At this point, there are no allocations
self.assertEqual({}, self.client.get_allocations_for_provider_tree(
self.context, self.compute_name))
# Create some allocations on our compute (with sharing)
cn_inst1_allocs = {
'allocations': {
self.compute_uuid: {'resources': {'MEMORY_MB': 512}},
uuids.numa1: {'resources': {'VCPU': 2, 'CUSTOM_PCPU': 2}},
uuids.ssp: {'resources': {'DISK_GB': 100}}
},
'consumer_generation': None,
'project_id': uuids.proj,
'user_id': uuids.user,
}
self.client.put('/allocations/' + uuids.cn_inst1, cn_inst1_allocs)
cn_inst2_allocs = {
'allocations': {
self.compute_uuid: {'resources': {'MEMORY_MB': 256}},
uuids.numa2: {'resources': {'CUSTOM_PCPU': 1}},
uuids.ssp: {'resources': {'DISK_GB': 50}}
},
'consumer_generation': None,
'project_id': uuids.proj,
'user_id': uuids.user,
}
self.client.put('/allocations/' + uuids.cn_inst2, cn_inst2_allocs)
# And on the other compute (with sharing)
self.client.put(
'/allocations/' + uuids.othercn_inst,
{'allocations': {
uuids.othercn: {'resources': {'VCPU': 2, 'MEMORY_MB': 64}},
uuids.ssp: {'resources': {'DISK_GB': 30}}
},
'consumer_generation': None,
'project_id': uuids.proj,
'user_id': uuids.user,
})
# And now we should get all the right allocations. Note that we see
# nothing from othercn_inst.
expected = {
uuids.cn_inst1: cn_inst1_allocs,
uuids.cn_inst2: cn_inst2_allocs,
}
actual = self.client.get_allocations_for_provider_tree(
self.context, self.compute_name)
# We don't care about the generations, and don't want to bother
# figuring out the right ones, so just remove those fields before
# checking equality
for allocs in list(expected.values()) + list(actual.values()):
del allocs['consumer_generation']
for alloc in allocs['allocations'].values():
if 'generation' in alloc:
del alloc['generation']
self.assertEqual(expected, actual)