[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:
Chris Dent
2016-09-02 15:58:38 +00:00
committed by Dan Smith
parent 2bd800ce25
commit 09627f2a0b
4 changed files with 91 additions and 27 deletions
@@ -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,
-6
View File
@@ -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 "
+25 -7
View File
@@ -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,