diff --git a/nova/exception.py b/nova/exception.py index b7c0d733d0c8..8c75f19c6b9c 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -2108,6 +2108,28 @@ class ResourceProviderInUse(NovaException): msg_fmt = _("Resource provider has allocations.") +class InvalidInventory(Invalid): + msg_fmt = _("Inventory for '%(resource_class)s' on " + "resource provider '%(resource_provider)s' invalid.") + + +class InventoryInUse(InvalidInventory): + msg_fmt = _("Inventory for '%(resource_classes)s' on " + "resource provider '%(resource_provider)s' in use.") + + +class InvalidInventoryCapacity(InvalidInventory): + msg_fmt = _("Invalid inventory for '%(resource_class)s' on " + "resource provider '%(resource_provider)s'. " + "The reserved value is greater than or equal to total.") + + +class InvalidInventoryNewCapacityExceeded(InvalidInventory): + msg_fmt = _("Invalid inventory for '%(resource_class)s' on " + "resource provider '%(resource_provider)s'. The new total " + "minus reserved amount is less than the existing used amount.") + + class UnsupportedPointerModelRequested(Invalid): msg_fmt = _("Pointer model '%(model)s' requested is not supported by " "host.") diff --git a/nova/objects/resource_provider.py b/nova/objects/resource_provider.py index 60d4496331b8..d4af5406812f 100644 --- a/nova/objects/resource_provider.py +++ b/nova/objects/resource_provider.py @@ -12,6 +12,7 @@ import six import sqlalchemy as sa +from sqlalchemy import func from sqlalchemy.orm import contains_eager from nova.db.sqlalchemy import api as db_api @@ -21,6 +22,7 @@ from nova import objects from nova.objects import base from nova.objects import fields +_ALLOC_TBL = models.Allocation.__table__ _INV_TBL = models.Inventory.__table__ _RP_TBL = models.ResourceProvider.__table__ @@ -42,11 +44,26 @@ 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([fields.ResourceClass.from_index( + allocation.resource_class) for allocation 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))) @@ -66,8 +83,9 @@ def _add_inventory_to_provider(conn, rp, inv_list, to_add): for res_class in to_add: inv_record = inv_list.find(res_class) if inv_record.capacity <= 0: - raise exception.ObjectActionError( - action='add inventory', reason='invalid resource capacity') + raise exception.InvalidInventoryCapacity( + resource_class=fields.ResourceClass.from_index(res_class), + resource_provider=rp.uuid) ins_stmt = _INV_TBL.insert().values( resource_provider_id=rp.id, resource_class_id=res_class, @@ -92,8 +110,19 @@ def _update_inventory_for_provider(conn, rp, inv_list, to_update): for res_class in to_update: inv_record = inv_list.find(res_class) if inv_record.capacity <= 0: - raise exception.ObjectActionError( - action='update inventory', reason='invalid resource capacity') + raise exception.InvalidInventoryCapacity( + resource_class=fields.ResourceClass.from_index(res_class), + resource_provider=rp.uuid) + allocation_query = sa.select( + [func.sum(_ALLOC_TBL.c.used).label('usage')]).\ + where(sa.and_( + _ALLOC_TBL.c.resource_provider_id == rp.id, + _ALLOC_TBL.c.resource_class_id == res_class)) + allocations = conn.execute(allocation_query).first() + if allocations and allocations['usage'] > inv_record.capacity: + raise exception.InvalidInventoryNewCapacityExceeded( + resource_class=fields.ResourceClass.from_index(res_class), + resource_provider=rp.uuid) upd_stmt = _INV_TBL.update().where(sa.and_( _INV_TBL.c.resource_provider_id == rp.id, _INV_TBL.c.resource_class_id == res_class)).values( diff --git a/nova/tests/functional/db/test_resource_provider.py b/nova/tests/functional/db/test_resource_provider.py index ac63c98d3fa8..3c9b7aa93c8a 100644 --- a/nova/tests/functional/db/test_resource_provider.py +++ b/nova/tests/functional/db/test_resource_provider.py @@ -290,8 +290,11 @@ class ResourceProviderTestCase(ResourceProviderBaseCase): total=2048, reserved=2048) disk_inv.obj_set_defaults() - self.assertRaises(exception.ObjectActionError, - rp.update_inventory, disk_inv) + error = self.assertRaises(exception.InvalidInventoryCapacity, + rp.update_inventory, disk_inv) + self.assertIn("Invalid inventory for '%s'" + % fields.ResourceClass.DISK_GB, str(error)) + self.assertIn("on resource provider '%s'." % rp.uuid, str(error)) # generation has not bumped self.assertEqual(saved_generation, rp.generation) @@ -347,6 +350,21 @@ class ResourceProviderTestCase(ResourceProviderBaseCase): self.assertIn('No inventory of class DISK_GB found for delete', str(error)) + def test_delete_inventory_with_allocation(self): + rp, allocation = self._make_allocation() + disk_inv = objects.Inventory(resource_provider=rp, + resource_class='DISK_GB', + total=2048) + disk_inv.obj_set_defaults() + inv_list = objects.InventoryList(objects=[disk_inv]) + rp.set_inventory(inv_list) + error = self.assertRaises(exception.InventoryInUse, + rp.delete_inventory, + 'DISK_GB') + self.assertIn( + "Inventory for 'DISK_GB' on resource provider '%s' in use" + % rp.uuid, str(error)) + def test_update_inventory_not_found(self): rp = objects.ResourceProvider(context=self.context, uuid=uuidsentinel.rp_uuid, @@ -361,6 +379,45 @@ class ResourceProviderTestCase(ResourceProviderBaseCase): self.assertIn('No inventory of class DISK_GB found for update', str(error)) + def test_update_inventory_violates_allocation(self): + rp, allocation = self._make_allocation() + disk_inv = objects.Inventory(resource_provider=rp, + resource_class='DISK_GB', + total=2048) + disk_inv.obj_set_defaults() + inv_list = objects.InventoryList(objects=[disk_inv]) + rp.set_inventory(inv_list) + # attempt to set inventory to less than currently allocated + # amounts + disk_inv = objects.Inventory( + resource_provider=rp, + resource_class=fields.ResourceClass.DISK_GB, total=1) + disk_inv.obj_set_defaults() + error = self.assertRaises( + exception.InvalidInventoryNewCapacityExceeded, + rp.update_inventory, disk_inv) + self.assertIn("Invalid inventory for '%s'" + % fields.ResourceClass.DISK_GB, str(error)) + self.assertIn("on resource provider '%s'." % rp.uuid, str(error)) + + def test_add_invalid_inventory(self): + rp = objects.ResourceProvider(context=self.context, + uuid=uuidsentinel.rp_uuid, + name=uuidsentinel.rp_name) + rp.create() + disk_inv = objects.Inventory( + resource_provider=rp, + resource_class=fields.ResourceClass.DISK_GB, + total=1024, reserved=2048) + disk_inv.obj_set_defaults() + error = self.assertRaises(exception.InvalidInventoryCapacity, + rp.add_inventory, + disk_inv) + self.assertIn("Invalid inventory for '%s'" + % fields.ResourceClass.DISK_GB, str(error)) + self.assertIn("on resource provider '%s'." + % rp.uuid, str(error)) + class ResourceProviderListTestCase(test.NoDBTestCase):