def _delete_inventory_from_provider(conn, rp, to_delete): """Deletes any inventory records from the supplied provider and set() of resource class identifiers. If there are allocations for any of the inventories to be deleted raise InventoryInUse exception. :param conn: DB connection to use. :param rp: Resource provider from which to delete inventory. :param to_delete: set() containing resource class IDs for records to delete. """ allocation_query = sa.select( [_ALLOC_TBL.c.resource_class_id.label('resource_class')]).where( sa.and_(_ALLOC_TBL.c.resource_provider_id == rp.id, _ALLOC_TBL.c.resource_class_id.in_(to_delete)) ).group_by(_ALLOC_TBL.c.resource_class_id) allocations = conn.execute(allocation_query).fetchall() if allocations: resource_classes = ', '.join([_RC_CACHE.string_from_id(alloc[0]) for alloc in allocations]) raise exception.InventoryInUse(resource_classes=resource_classes, resource_provider=rp.uuid) del_stmt = _INV_TBL.delete().where(sa.and_( _INV_TBL.c.resource_provider_id == rp.id, _INV_TBL.c.resource_class_id.in_(to_delete))) res = conn.execute(del_stmt) return res.rowcount
def test_delete_inventory_inventory_in_use(self, mock_get, mock_put, mock_warn): cn = self.compute_node rp = objects.ResourceProvider(uuid=cn.uuid, generation=42) # Make sure the ResourceProvider exists for preventing to call the API self.client._resource_providers[cn.uuid] = rp mock_get.return_value.json.return_value = { 'resource_provider_generation': 1, 'inventories': { 'VCPU': { 'total': 16 }, 'MEMORY_MB': { 'total': 1024 }, 'DISK_GB': { 'total': 10 }, } } mock_put.return_value.status_code = 409 rc_str = "VCPU, MEMORY_MB" in_use_exc = exception.InventoryInUse( resource_classes=rc_str, resource_provider=cn.uuid, ) fault_text = """ 409 Conflict There was a conflict when trying to complete your request. update conflict: %s """ % six.text_type(in_use_exc) mock_put.return_value.text = fault_text mock_put.return_value.json.return_value = { 'resource_provider_generation': 44, 'inventories': {} } result = self.client._delete_inventory(cn.uuid) self.assertIsNone(result) self.assertTrue(mock_warn.called)
def _update_inventory_attempt(self, rp_uuid, inv_data): """Update the inventory for this resource provider if needed. :param rp_uuid: The resource provider UUID for the operation :param inv_data: The new inventory for the resource provider :returns: True if the inventory was updated (or did not need to be), False otherwise. """ curr = self._get_inventory_and_update_provider_generation(rp_uuid) # Check to see if we need to update placement's view if inv_data == curr.get('inventories', {}): return True cur_rp_gen = self._resource_providers[rp_uuid]['generation'] payload = { 'resource_provider_generation': cur_rp_gen, 'inventories': inv_data, } url = '/resource_providers/%s/inventories' % rp_uuid result = self.put(url, payload) if result.status_code == 409: LOG.info( _LI('[%(placement_req_id)s] Inventory update conflict ' 'for %(resource_provider_uuid)s with generation ID ' '%(generation_id)s'), { 'placement_req_id': get_placement_request_id(result), 'resource_provider_uuid': rp_uuid, 'generation_id': cur_rp_gen }) # NOTE(jaypipes): There may be cases when we try to set a # provider's inventory that results in attempting to delete an # inventory record for a resource class that has an active # allocation. We need to catch this particular case and raise an # exception here instead of returning False, since we should not # re-try the operation in this case. # # A use case for where this can occur is the following: # # 1) Provider created for each Ironic baremetal node in Newton # 2) Inventory records for baremetal node created for VCPU, # MEMORY_MB and DISK_GB # 3) A Nova instance consumes the baremetal node and allocation # records are created for VCPU, MEMORY_MB and DISK_GB matching # the total amount of those resource on the baremetal node. # 3) Upgrade to Ocata and now resource tracker wants to set the # provider's inventory to a single record of resource class # CUSTOM_IRON_SILVER (or whatever the Ironic node's # "resource_class" attribute is) # 4) Scheduler report client sends the inventory list containing a # single CUSTOM_IRON_SILVER record and placement service # attempts to delete the inventory records for VCPU, MEMORY_MB # and DISK_GB. An exception is raised from the placement service # because allocation records exist for those resource classes, # and a 409 Conflict is returned to the compute node. We need to # trigger a delete of the old allocation records and then set # the new inventory, and then set the allocation record to the # new CUSTOM_IRON_SILVER record. match = _RE_INV_IN_USE.search(result.text) if match: rc = match.group(1) raise exception.InventoryInUse( resource_classes=rc, resource_provider=rp_uuid, ) # Invalidate our cache and re-fetch the resource provider # to be sure to get the latest generation. del self._resource_providers[rp_uuid] # NOTE(jaypipes): We don't need to pass a name parameter to # _ensure_resource_provider() because we know the resource provider # record already exists. We're just reloading the record here. self._ensure_resource_provider(rp_uuid) return False elif not result: placement_req_id = get_placement_request_id(result) LOG.warning( _LW('[%(placement_req_id)s] Failed to update ' 'inventory for resource provider ' '%(uuid)s: %(status)i %(text)s'), { 'placement_req_id': placement_req_id, 'uuid': rp_uuid, 'status': result.status_code, 'text': result.text }) # log the body at debug level LOG.debug( '[%(placement_req_id)s] Failed inventory update request ' 'for resource provider %(uuid)s with body: %(payload)s', { 'placement_req_id': placement_req_id, 'uuid': rp_uuid, 'payload': payload }) return False if result.status_code != 200: placement_req_id = get_placement_request_id(result) LOG.info( _LI('[%(placement_req_id)s] Received unexpected response code ' '%(code)i while trying to update inventory for resource ' 'provider %(uuid)s: %(text)s'), { 'placement_req_id': placement_req_id, 'uuid': rp_uuid, 'code': result.status_code, 'text': result.text }) return False # Update our view of the generation for next time updated_inventories_result = result.json() new_gen = updated_inventories_result['resource_provider_generation'] self._resource_providers[rp_uuid]['generation'] = new_gen LOG.debug('Updated inventory for %s at generation %i', rp_uuid, new_gen) return True