diff --git a/nova/scheduler/client/report.py b/nova/scheduler/client/report.py index 2e1d5516ebc0..0fc6cd4f2b1e 100644 --- a/nova/scheduler/client/report.py +++ b/nova/scheduler/client/report.py @@ -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. diff --git a/nova/tests/functional/api/openstack/placement/test_report_client.py b/nova/tests/functional/api/openstack/placement/test_report_client.py index 8fd2835a6594..79ebe6dc3d5c 100644 --- a/nova/tests/functional/api/openstack/placement/test_report_client.py +++ b/nova/tests/functional/api/openstack/placement/test_report_client.py @@ -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'])