New-style _set_inventory_for_provider
Given the UUID of a provider, set the inventory records for the provider to the supplied dict of resources. Compare and contrast with the existing set_inventory_for_provider, which was created for the simpler get_inventory code path from the resource tracker. This one is specially formulated for use by update_from_provider_tree, because from there, whereas we DO need to _ensure_resource_class(), we ALSO want to short out if inv_data matches what's already cached, but we DON'T want to: - _ensure_resource_provider - refresh_and_get_inventory - DELETE if inventory is empty, else PUT - retry - invalidate the cache on failure This version is more in the spirit of set_traits_for_provider. Change-Id: I45f1df6ca5618f76337319ba225493625a5ee2d6 blueprint: update-provider-tree
This commit is contained in:
parent
a0dc24dc1a
commit
3921cbc9ec
|
@ -1030,6 +1030,113 @@ class SchedulerReportClient(object):
|
||||||
# when we invoke the DELETE. See bug #1746374.
|
# when we invoke the DELETE. See bug #1746374.
|
||||||
self._update_inventory(context, rp_uuid, inv_data)
|
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
|
@safe_connect
|
||||||
def _ensure_traits(self, context, traits):
|
def _ensure_traits(self, context, traits):
|
||||||
"""Make sure all specified traits exist in the placement service.
|
"""Make sure all specified traits exist in the placement service.
|
||||||
|
|
|
@ -449,3 +449,240 @@ class SchedulerReportClientTests(test.TestCase):
|
||||||
uuids.sbw, [uuids.agg_bw]))
|
uuids.sbw, [uuids.agg_bw]))
|
||||||
self.assertFalse(prov_tree.have_aggregates_changed(
|
self.assertFalse(prov_tree.have_aggregates_changed(
|
||||||
self.compute_uuid, [uuids.agg_disk_1, uuids.agg_disk_2]))
|
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'])
|
||||||
|
|
Loading…
Reference in New Issue