[placement] Allow inventory to violate allocations
When a compute node is reconfigured to have different inventory it needs to be able to tell the placement service of these changes, even if they violate existing allocations. Thus, when updating inventory we no longer check the new capacity against existing allocations, we just let it happen (but log a warning). This is safe because allocations in excess of capacity will still prevent resources from being used. Change-Id: I48ae1d7cd6bc309243493ddac99ce990c0146534 Closes-Bug: #1619722 Co-Authored-By: Dan Smith <dansmith@redhat.com>
This commit is contained in:
@@ -317,8 +317,6 @@ def set_inventories(req):
|
||||
|
||||
If the resource generation is out of sync, return a 409.
|
||||
If an inventory to be deleted is in use, return a 409.
|
||||
If an inventory to be updated would set capacity to exceed existing
|
||||
use, return a 409.
|
||||
If any inventory to be created or updated has settings which are
|
||||
invalid (for example reserved exceeds capacity), return a 400.
|
||||
|
||||
@@ -347,7 +345,6 @@ def set_inventories(req):
|
||||
resource_provider.set_inventory(inventories)
|
||||
except (exception.ConcurrentUpdateDetected,
|
||||
exception.InventoryInUse,
|
||||
exception.InvalidInventoryNewCapacityExceeded,
|
||||
db_exc.DBDuplicateEntry) as exc:
|
||||
raise webob.exc.HTTPConflict(
|
||||
'update conflict: %s' % exc,
|
||||
@@ -367,8 +364,6 @@ def update_inventory(req):
|
||||
"""PUT to update one inventory.
|
||||
|
||||
If the resource generation is out of sync, return a 409.
|
||||
If the inventory would set capacity to exceed existing use, return
|
||||
a 409.
|
||||
If the inventory has settings which are invalid (for example
|
||||
reserved exceeds capacity), return a 400.
|
||||
|
||||
@@ -394,7 +389,6 @@ def update_inventory(req):
|
||||
try:
|
||||
resource_provider.update_inventory(inventory)
|
||||
except (exception.ConcurrentUpdateDetected,
|
||||
exception.InvalidInventoryNewCapacityExceeded,
|
||||
db_exc.DBDuplicateEntry) as exc:
|
||||
raise webob.exc.HTTPConflict(
|
||||
'update conflict: %s' % exc,
|
||||
|
||||
@@ -2129,12 +2129,6 @@ class InvalidInventoryCapacity(InvalidInventory):
|
||||
"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 InvalidAllocationCapacityExceeded(InvalidInventory):
|
||||
msg_fmt = _("Unable to create allocation for '%(resource_class)s' on "
|
||||
"resource provider '%(resource_provider)s'. The requested "
|
||||
|
||||
@@ -111,7 +111,10 @@ def _update_inventory_for_provider(conn, rp, inv_list, to_update):
|
||||
:param inv_list: InventoryList object
|
||||
:param to_update: set() containing resource class IDs to search inv_list
|
||||
for updating in resource provider.
|
||||
:returns: A list of (uuid, class) tuples that have exceeded their
|
||||
capacity after this inventory update.
|
||||
"""
|
||||
exceeded = []
|
||||
for res_class in to_update:
|
||||
inv_record = inv_list.find(res_class)
|
||||
if inv_record.capacity <= 0:
|
||||
@@ -125,9 +128,8 @@ def _update_inventory_for_provider(conn, rp, inv_list, to_update):
|
||||
_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)
|
||||
exceeded.append((rp.uuid,
|
||||
fields.ResourceClass.from_index(res_class)))
|
||||
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(
|
||||
@@ -142,6 +144,7 @@ def _update_inventory_for_provider(conn, rp, inv_list, to_update):
|
||||
raise exception.NotFound(
|
||||
'No inventory of class %s found for update'
|
||||
% fields.ResourceClass.from_index(res_class))
|
||||
return exceeded
|
||||
|
||||
|
||||
def _increment_provider_generation(conn, rp):
|
||||
@@ -188,9 +191,10 @@ def _update_inventory(context, rp, inventory):
|
||||
inv_list = InventoryList(objects=[inventory])
|
||||
conn = context.session.connection()
|
||||
with conn.begin():
|
||||
_update_inventory_for_provider(
|
||||
exceeded = _update_inventory_for_provider(
|
||||
conn, rp, inv_list, set([resource_class_id]))
|
||||
rp.generation = _increment_provider_generation(conn, rp)
|
||||
return exceeded
|
||||
|
||||
|
||||
@db_api.api_context_manager.writer
|
||||
@@ -215,6 +219,8 @@ def _set_inventory(context, rp, inv_list):
|
||||
:param context: Nova RequestContext.
|
||||
:param rp: `ResourceProvider` object upon which to set inventory.
|
||||
:param inv_list: `InventoryList` object to save to backend storage.
|
||||
:returns: A list of (uuid, class) tuples that have exceeded their
|
||||
capacity after this inventory update.
|
||||
:raises nova.exception.ConcurrentUpdateDetected: if another thread updated
|
||||
the same resource provider's view of its inventory or allocations
|
||||
in between the time when this object was originally read
|
||||
@@ -233,6 +239,7 @@ def _set_inventory(context, rp, inv_list):
|
||||
to_add = these_resources - existing_resources
|
||||
to_delete = existing_resources - these_resources
|
||||
to_update = these_resources & existing_resources
|
||||
exceeded = []
|
||||
|
||||
with conn.begin():
|
||||
if to_delete:
|
||||
@@ -240,7 +247,8 @@ def _set_inventory(context, rp, inv_list):
|
||||
if to_add:
|
||||
_add_inventory_to_provider(conn, rp, inv_list, to_add)
|
||||
if to_update:
|
||||
_update_inventory_for_provider(conn, rp, inv_list, to_update)
|
||||
exceeded = _update_inventory_for_provider(conn, rp, inv_list,
|
||||
to_update)
|
||||
|
||||
# Here is where we update the resource provider's generation value.
|
||||
# If this update updates zero rows, that means that another
|
||||
@@ -253,6 +261,8 @@ def _set_inventory(context, rp, inv_list):
|
||||
# conditions and re-reading the existing inventory information.
|
||||
rp.generation = _increment_provider_generation(conn, rp)
|
||||
|
||||
return exceeded
|
||||
|
||||
|
||||
@base.NovaObjectRegistry.register
|
||||
class ResourceProvider(base.NovaObject):
|
||||
@@ -320,7 +330,11 @@ class ResourceProvider(base.NovaObject):
|
||||
@base.remotable
|
||||
def set_inventory(self, inv_list):
|
||||
"""Set all resource provider Inventory to be the provided list."""
|
||||
_set_inventory(self._context, self, inv_list)
|
||||
exceeded = _set_inventory(self._context, self, inv_list)
|
||||
for uuid, rclass in exceeded:
|
||||
LOG.warning(_LW('Resource provider %(uuid)s is now over-'
|
||||
'capacity for %(resource)s'),
|
||||
{'uuid': uuid, 'resource': rclass})
|
||||
self.obj_reset_changes()
|
||||
|
||||
@base.remotable
|
||||
@@ -329,7 +343,11 @@ class ResourceProvider(base.NovaObject):
|
||||
|
||||
Fails if no Inventory of the same class is present.
|
||||
"""
|
||||
_update_inventory(self._context, self, inventory)
|
||||
exceeded = _update_inventory(self._context, self, inventory)
|
||||
for uuid, rclass in exceeded:
|
||||
LOG.warning(_LW('Resource provider %(uuid)s is now over-'
|
||||
'capacity for %(resource)s'),
|
||||
{'uuid': uuid, 'resource': rclass})
|
||||
self.obj_reset_changes()
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
# under the License.
|
||||
|
||||
|
||||
import mock
|
||||
from oslo_db import exception as db_exc
|
||||
|
||||
from nova import context
|
||||
@@ -205,6 +206,53 @@ class ResourceProviderTestCase(ResourceProviderBaseCase):
|
||||
self.context, resource_provider.uuid))
|
||||
self.assertEqual(33, reloaded_inventories[0].total)
|
||||
|
||||
@mock.patch('nova.objects.resource_provider.LOG')
|
||||
def test_set_inventory_over_capacity(self, mock_log):
|
||||
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=15,
|
||||
min_unit=10,
|
||||
max_unit=100,
|
||||
step_size=10,
|
||||
allocation_ratio=1.0)
|
||||
vcpu_inv = objects.Inventory(
|
||||
resource_provider=rp,
|
||||
resource_class=fields.ResourceClass.VCPU,
|
||||
total=12,
|
||||
reserved=0,
|
||||
min_unit=1,
|
||||
max_unit=12,
|
||||
step_size=1,
|
||||
allocation_ratio=16.0)
|
||||
|
||||
inv_list = objects.InventoryList(objects=[disk_inv, vcpu_inv])
|
||||
rp.set_inventory(inv_list)
|
||||
self.assertFalse(mock_log.warning.called)
|
||||
|
||||
# Allocate something reasonable for the above inventory
|
||||
alloc = objects.Allocation(
|
||||
context=self.context,
|
||||
resource_provider=rp,
|
||||
consumer_id=uuidsentinel.consumer,
|
||||
resource_class='DISK_GB',
|
||||
used=512)
|
||||
alloc.create()
|
||||
|
||||
# Update our inventory to over-subscribe us after the above allocation
|
||||
disk_inv.total = 400
|
||||
rp.set_inventory(inv_list)
|
||||
|
||||
# We should succeed, but have logged a warning for going over on disk
|
||||
mock_log.warning.assert_called_once_with(
|
||||
mock.ANY, {'uuid': rp.uuid, 'resource': 'DISK_GB'})
|
||||
|
||||
def test_provider_modify_inventory(self):
|
||||
rp = objects.ResourceProvider(context=self.context,
|
||||
uuid=uuidsentinel.rp_uuid,
|
||||
@@ -380,7 +428,11 @@ class ResourceProviderTestCase(ResourceProviderBaseCase):
|
||||
self.assertIn('No inventory of class DISK_GB found for update',
|
||||
str(error))
|
||||
|
||||
def test_update_inventory_violates_allocation(self):
|
||||
@mock.patch('nova.objects.resource_provider.LOG')
|
||||
def test_update_inventory_violates_allocation(self, mock_log):
|
||||
# Compute nodes that are reconfigured have to be able to set
|
||||
# their inventory to something that violates allocations so
|
||||
# we need to make that possible.
|
||||
rp, allocation = self._make_allocation()
|
||||
disk_inv = objects.Inventory(resource_provider=rp,
|
||||
resource_class='DISK_GB',
|
||||
@@ -390,16 +442,22 @@ class ResourceProviderTestCase(ResourceProviderBaseCase):
|
||||
rp.set_inventory(inv_list)
|
||||
# attempt to set inventory to less than currently allocated
|
||||
# amounts
|
||||
new_total = 1
|
||||
disk_inv = objects.Inventory(
|
||||
resource_provider=rp,
|
||||
resource_class=fields.ResourceClass.DISK_GB, total=1)
|
||||
resource_class=fields.ResourceClass.DISK_GB, total=new_total)
|
||||
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))
|
||||
rp.update_inventory(disk_inv)
|
||||
|
||||
usages = objects.UsageList.get_all_by_resource_provider_uuid(
|
||||
self.context, rp.uuid)
|
||||
self.assertEqual(allocation.used, usages[0].usage)
|
||||
|
||||
inv_list = objects.InventoryList.get_all_by_resource_provider_uuid(
|
||||
self.context, rp.uuid)
|
||||
self.assertEqual(new_total, inv_list[0].total)
|
||||
mock_log.warning.assert_called_once_with(
|
||||
mock.ANY, {'uuid': rp.uuid, 'resource': 'DISK_GB'})
|
||||
|
||||
def test_add_invalid_inventory(self):
|
||||
rp = objects.ResourceProvider(context=self.context,
|
||||
|
||||
Reference in New Issue
Block a user