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.") 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): class UnsupportedPointerModelRequested(Invalid):
msg_fmt = _("Pointer model '%(model)s' requested is not supported by " msg_fmt = _("Pointer model '%(model)s' requested is not supported by "
"host.") "host.")

View File

@ -12,6 +12,7 @@
import six import six
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import func
from sqlalchemy.orm import contains_eager from sqlalchemy.orm import contains_eager
from nova.db.sqlalchemy import api as db_api 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 base
from nova.objects import fields from nova.objects import fields
_ALLOC_TBL = models.Allocation.__table__
_INV_TBL = models.Inventory.__table__ _INV_TBL = models.Inventory.__table__
_RP_TBL = models.ResourceProvider.__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 """Deletes any inventory records from the supplied provider and set() of
resource class identifiers. 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 conn: DB connection to use.
:param rp: Resource provider from which to delete inventory. :param rp: Resource provider from which to delete inventory.
:param to_delete: set() containing resource class IDs for records to :param to_delete: set() containing resource class IDs for records to
delete. 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_( del_stmt = _INV_TBL.delete().where(sa.and_(
_INV_TBL.c.resource_provider_id == rp.id, _INV_TBL.c.resource_provider_id == rp.id,
_INV_TBL.c.resource_class_id.in_(to_delete))) _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: for res_class in to_add:
inv_record = inv_list.find(res_class) inv_record = inv_list.find(res_class)
if inv_record.capacity <= 0: if inv_record.capacity <= 0:
raise exception.ObjectActionError( raise exception.InvalidInventoryCapacity(
action='add inventory', reason='invalid resource capacity') resource_class=fields.ResourceClass.from_index(res_class),
resource_provider=rp.uuid)
ins_stmt = _INV_TBL.insert().values( ins_stmt = _INV_TBL.insert().values(
resource_provider_id=rp.id, resource_provider_id=rp.id,
resource_class_id=res_class, 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: for res_class in to_update:
inv_record = inv_list.find(res_class) inv_record = inv_list.find(res_class)
if inv_record.capacity <= 0: if inv_record.capacity <= 0:
raise exception.ObjectActionError( raise exception.InvalidInventoryCapacity(
action='update inventory', reason='invalid resource capacity') 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_( upd_stmt = _INV_TBL.update().where(sa.and_(
_INV_TBL.c.resource_provider_id == rp.id, _INV_TBL.c.resource_provider_id == rp.id,
_INV_TBL.c.resource_class_id == res_class)).values( _INV_TBL.c.resource_class_id == res_class)).values(

View File

@ -290,8 +290,11 @@ class ResourceProviderTestCase(ResourceProviderBaseCase):
total=2048, total=2048,
reserved=2048) reserved=2048)
disk_inv.obj_set_defaults() disk_inv.obj_set_defaults()
self.assertRaises(exception.ObjectActionError, error = self.assertRaises(exception.InvalidInventoryCapacity,
rp.update_inventory, disk_inv) 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 # generation has not bumped
self.assertEqual(saved_generation, rp.generation) self.assertEqual(saved_generation, rp.generation)
@ -347,6 +350,21 @@ class ResourceProviderTestCase(ResourceProviderBaseCase):
self.assertIn('No inventory of class DISK_GB found for delete', self.assertIn('No inventory of class DISK_GB found for delete',
str(error)) 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): def test_update_inventory_not_found(self):
rp = objects.ResourceProvider(context=self.context, rp = objects.ResourceProvider(context=self.context,
uuid=uuidsentinel.rp_uuid, uuid=uuidsentinel.rp_uuid,
@ -361,6 +379,45 @@ class ResourceProviderTestCase(ResourceProviderBaseCase):
self.assertIn('No inventory of class DISK_GB found for update', self.assertIn('No inventory of class DISK_GB found for update',
str(error)) 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): class ResourceProviderListTestCase(test.NoDBTestCase):