Check capacity and allocations when changing Inventory

Four new exceptions are added, three as a subclass of
InvalidInventory which is itself a subclass of Invalid:

* InventoryInUse: raised when there are allocations for an inventory
  being deleted.
* InvalidInventoryCapacity: raised when reserved is greater or equal
  to capacity.
* InvalidInventoryNewCapacityExceeded: raised when an inventory is
  changed in a way that makes usage exceed capacity.

Change-Id: I41bd4184484226ce04e28848ff7919c8616f268d
Partially-Implements: blueprint generic-resource-pools
This commit is contained in:
Chris Dent 2016-07-11 20:58:20 +00:00
parent 33a50eace0
commit cee4348bd5
3 changed files with 114 additions and 6 deletions

View File

@ -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.")

View File

@ -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(

View File

@ -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):