Merge "New-style _set_inventory_for_provider"

This commit is contained in:
Zuul 2018-03-16 13:22:06 +00:00 committed by Gerrit Code Review
commit 49a988182f
2 changed files with 344 additions and 0 deletions

View File

@ -1030,6 +1030,113 @@ class SchedulerReportClient(object):
# when we invoke the DELETE. See bug #1746374.
self._update_inventory(context, rp_uuid, inv_data)
def _set_inventory_for_provider(self, context, rp_uuid, inv_data):
"""Given the UUID of a provider, set the inventory records for the
provider to the supplied dict of resources.
Compare and contrast with set_inventory_for_provider above. This one
is specially formulated for use by update_from_provider_tree. Like the
other method, we DO need to _ensure_resource_class - i.e. automatically
create new resource classes specified in the inv_data. However, UNLIKE
the other method:
- We don't use the DELETE API when inventory is empty, because that guy
doesn't return content, and we need to update the cached provider
tree with the new generation.
- We raise exceptions (rather than returning a boolean) which are
handled in a consistent fashion by update_from_provider_tree.
- We don't invalidate the cache on failure. That's controlled at a
broader scope (based on errors from ANY of the set_*_for_provider
methods, etc.) by update_from_provider_tree.
- We don't retry. In this code path, retries happen at the level of
the resource tracker on the next iteration.
- We take advantage of the cache and no-op if inv_data isn't different
from what we have locally. This is an optimization, not essential.
- We don't _ensure_resource_provider or refresh_and_get_inventory,
because that's already been done in the code paths leading up to
update_from_provider_tree (by get_provider_tree). This is an
optimization, not essential.
In short, this version is more in the spirit of set_traits_for_provider
and set_aggregates_for_provider.
:param context: The security context
:param rp_uuid: The UUID of the provider whose inventory is to be
updated.
:param inv_data: Dict, keyed by resource class name, of inventory data
to set for the provider. Use None or the empty dict
to remove all inventory for the provider.
:raises: InventoryInUse if inv_data indicates removal of inventory in a
resource class which has active allocations for this provider.
:raises: InvalidResourceClass if inv_data contains a resource class
which cannot be created.
:raises: ResourceProviderUpdateConflict if the provider's generation
doesn't match the generation in the cache. Callers may choose
to retrieve the provider and its associations afresh and
redrive this operation.
:raises: ResourceProviderUpdateFailed on any other placement API
failure.
"""
# TODO(efried): Consolidate/refactor to one set_inventory_for_provider.
# NOTE(efried): This is here because _ensure_resource_class already has
# @safe_connect, so we don't want to decorate this whole method with it
@safe_connect
def do_put(url, payload):
return self.put(url, payload, global_request_id=context.global_id)
# If not different from what we've got, short out
if not self._provider_tree.has_inventory_changed(rp_uuid, inv_data):
return
# Ensure non-standard resource classes exist, creating them if needed.
self._ensure_resource_classes(context, set(inv_data))
url = '/resource_providers/%s/inventories' % rp_uuid
inv_data = inv_data or {}
generation = self._provider_tree.data(rp_uuid).generation
payload = {
'resource_provider_generation': generation,
'inventories': inv_data,
}
resp = do_put(url, payload)
if resp.status_code == 200:
json = resp.json()
self._provider_tree.update_inventory(
rp_uuid, json['inventories'],
generation=json['resource_provider_generation'])
return
# Some error occurred; log it
msg = ("[%(placement_req_id)s] Failed to update inventory to "
"[%(inv_data)s] for resource provider with UUID %(uuid)s. Got "
"%(status_code)d: %(err_text)s")
args = {
'placement_req_id': get_placement_request_id(resp),
'uuid': rp_uuid,
'inv_data': str(inv_data),
'status_code': resp.status_code,
'err_text': resp.text,
}
LOG.error(msg, args)
if resp.status_code == 409:
# If a conflict attempting to remove inventory in a resource class
# with active allocations, raise InventoryInUse
match = _RE_INV_IN_USE.search(resp.text)
if match:
rc = match.group(1)
raise exception.InventoryInUse(
resource_classes=rc,
resource_provider=rp_uuid,
)
# Other conflicts are generation mismatch: raise conflict exception
raise exception.ResourceProviderUpdateConflict(
uuid=rp_uuid, generation=generation, error=resp.text)
# Otherwise, raise generic exception
raise exception.ResourceProviderUpdateFailed(url=url, error=resp.text)
@safe_connect
def _ensure_traits(self, context, traits):
"""Make sure all specified traits exist in the placement service.

View File

@ -449,3 +449,240 @@ class SchedulerReportClientTests(test.TestCase):
uuids.sbw, [uuids.agg_bw]))
self.assertFalse(prov_tree.have_aggregates_changed(
self.compute_uuid, [uuids.agg_disk_1, uuids.agg_disk_2]))
def test__set_inventory_for_provider(self):
"""Tests for SchedulerReportClient._set_inventory_for_provider, NOT
set_inventory_for_provider.
"""
with self._interceptor():
inv = {
fields.ResourceClass.SRIOV_NET_VF: {
'total': 24,
'reserved': 1,
'min_unit': 1,
'max_unit': 24,
'step_size': 1,
'allocation_ratio': 1.0,
},
}
# Provider doesn't exist in our cache
self.assertRaises(
ValueError,
self.client._set_inventory_for_provider,
self.context, uuids.cn, inv)
self.assertIsNone(self.client._get_inventory(
self.context, uuids.cn))
# Create the provider
self.client._ensure_resource_provider(self.context, uuids.cn)
# Still no inventory, but now we don't get a 404
self.assertEqual(
{},
self.client._get_inventory(
self.context, uuids.cn)['inventories'])
# Now set the inventory
self.client._set_inventory_for_provider(
self.context, uuids.cn, inv)
self.assertEqual(
inv,
self.client._get_inventory(
self.context, uuids.cn)['inventories'])
# Make sure we can change it
inv = {
fields.ResourceClass.SRIOV_NET_VF: {
'total': 24,
'reserved': 1,
'min_unit': 1,
'max_unit': 24,
'step_size': 1,
'allocation_ratio': 1.0,
},
fields.ResourceClass.IPV4_ADDRESS: {
'total': 128,
'reserved': 0,
'min_unit': 1,
'max_unit': 8,
'step_size': 1,
'allocation_ratio': 1.0,
},
}
self.client._set_inventory_for_provider(
self.context, uuids.cn, inv)
self.assertEqual(
inv,
self.client._get_inventory(
self.context, uuids.cn)['inventories'])
# Create custom resource classes on the fly
self.assertFalse(
self.client.get('/resource_classes/CUSTOM_BANDWIDTH'))
inv = {
fields.ResourceClass.SRIOV_NET_VF: {
'total': 24,
'reserved': 1,
'min_unit': 1,
'max_unit': 24,
'step_size': 1,
'allocation_ratio': 1.0,
},
fields.ResourceClass.IPV4_ADDRESS: {
'total': 128,
'reserved': 0,
'min_unit': 1,
'max_unit': 8,
'step_size': 1,
'allocation_ratio': 1.0,
},
'CUSTOM_BANDWIDTH': {
'total': 1250000,
'reserved': 10000,
'min_unit': 5000,
'max_unit': 250000,
'step_size': 5000,
'allocation_ratio': 8.0,
},
}
self.client._set_inventory_for_provider(
self.context, uuids.cn, inv)
self.assertEqual(
inv,
self.client._get_inventory(
self.context, uuids.cn)['inventories'])
# The custom resource class got created.
self.assertTrue(
self.client.get('/resource_classes/CUSTOM_BANDWIDTH'))
# Creating a bogus resource class raises the appropriate exception.
bogus_inv = dict(inv)
bogus_inv['CUSTOM_BOGU$$'] = {
'total': 1,
'reserved': 1,
'min_unit': 1,
'max_unit': 1,
'step_size': 1,
'allocation_ratio': 1.0,
}
self.assertRaises(
exception.InvalidResourceClass,
self.client._set_inventory_for_provider,
self.context, uuids.cn, bogus_inv)
self.assertFalse(
self.client.get('/resource_classes/BOGUS'))
self.assertEqual(
inv,
self.client._get_inventory(
self.context, uuids.cn)['inventories'])
# Create a generation conflict by doing an "out of band" update
oob_inv = {
fields.ResourceClass.IPV4_ADDRESS: {
'total': 128,
'reserved': 0,
'min_unit': 1,
'max_unit': 8,
'step_size': 1,
'allocation_ratio': 1.0,
},
}
gen = self.client._provider_tree.data(uuids.cn).generation
self.assertTrue(
self.client.put(
'/resource_providers/%s/inventories' % uuids.cn,
{'resource_provider_generation': gen,
'inventories': oob_inv}))
self.assertEqual(
oob_inv,
self.client._get_inventory(
self.context, uuids.cn)['inventories'])
# Now try to update again.
inv = {
fields.ResourceClass.SRIOV_NET_VF: {
'total': 24,
'reserved': 1,
'min_unit': 1,
'max_unit': 24,
'step_size': 1,
'allocation_ratio': 1.0,
},
'CUSTOM_BANDWIDTH': {
'total': 1250000,
'reserved': 10000,
'min_unit': 5000,
'max_unit': 250000,
'step_size': 5000,
'allocation_ratio': 8.0,
},
}
# Cached generation is off, so this will bounce with a conflict.
self.assertRaises(
exception.ResourceProviderUpdateConflict,
self.client._set_inventory_for_provider,
self.context, uuids.cn, inv)
# Inventory still corresponds to the out-of-band update
self.assertEqual(
oob_inv,
self.client._get_inventory(
self.context, uuids.cn)['inventories'])
# Force refresh to get the latest generation
self.client._refresh_and_get_inventory(self.context, uuids.cn)
# Now the update should work
self.client._set_inventory_for_provider(
self.context, uuids.cn, inv)
self.assertEqual(
inv,
self.client._get_inventory(
self.context, uuids.cn)['inventories'])
# Now set up an InventoryInUse case by creating a VF allocation...
self.assertTrue(
self.client.put_allocations(
self.context, uuids.cn, uuids.consumer,
{fields.ResourceClass.SRIOV_NET_VF: 1},
uuids.proj, uuids.user))
# ...and trying to delete the provider's VF inventory
bad_inv = {
'CUSTOM_BANDWIDTH': {
'total': 1250000,
'reserved': 10000,
'min_unit': 5000,
'max_unit': 250000,
'step_size': 5000,
'allocation_ratio': 8.0,
},
}
# Allocation bumped the generation, so refresh to get the latest
self.client._refresh_and_get_inventory(self.context, uuids.cn)
self.assertRaises(
exception.InventoryInUse,
self.client._set_inventory_for_provider,
self.context, uuids.cn, bad_inv)
self.assertEqual(
inv,
self.client._get_inventory(
self.context, uuids.cn)['inventories'])
# Same result if we try to clear all the inventory
bad_inv = {}
self.assertRaises(
exception.InventoryInUse,
self.client._set_inventory_for_provider,
self.context, uuids.cn, bad_inv)
self.assertEqual(
inv,
self.client._get_inventory(
self.context, uuids.cn)['inventories'])
# Remove the allocation to make it work
self.client.delete('/allocations/' + uuids.consumer)
# Force refresh to get the latest generation
self.client._refresh_and_get_inventory(self.context, uuids.cn)
inv = {}
self.client._set_inventory_for_provider(
self.context, uuids.cn, inv)
self.assertEqual(
inv,
self.client._get_inventory(
self.context, uuids.cn)['inventories'])