diff --git a/nova/objects/resource_provider.py b/nova/objects/resource_provider.py index d16f3d738ab0..5d09de18972d 100644 --- a/nova/objects/resource_provider.py +++ b/nova/objects/resource_provider.py @@ -75,7 +75,8 @@ def _delete_inventory_from_provider(conn, rp, to_delete): 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))) - conn.execute(del_stmt) + res = conn.execute(del_stmt) + return res.rowcount def _add_inventory_to_provider(conn, rp, inv_list, to_add): @@ -89,6 +90,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') ins_stmt = _INV_TBL.insert().values( resource_provider_id=rp.id, resource_class_id=res_class, @@ -112,6 +116,9 @@ 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') 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( @@ -121,7 +128,11 @@ def _update_inventory_for_provider(conn, rp, inv_list, to_update): max_unit=inv_record.max_unit, step_size=inv_record.step_size, allocation_ratio=inv_record.allocation_ratio) - conn.execute(upd_stmt) + res = conn.execute(upd_stmt) + if not res.rowcount: + raise exception.NotFound( + 'No inventory of class %s found for update' + % fields.ResourceClass.from_index(res_class)) def _increment_provider_generation(conn, rp): @@ -149,6 +160,43 @@ def _increment_provider_generation(conn, rp): return new_generation +@db_api.api_context_manager.writer +def _add_inventory(context, rp, inventory): + """Add one Inventory that wasn't already on the provider.""" + resource_class_id = fields.ResourceClass.index(inventory.resource_class) + inv_list = InventoryList(objects=[inventory]) + conn = context.session.connection() + with conn.begin(): + _add_inventory_to_provider( + conn, rp, inv_list, set([resource_class_id])) + rp.generation = _increment_provider_generation(conn, rp) + + +@db_api.api_context_manager.writer +def _update_inventory(context, rp, inventory): + """Update an inventory already on the provider.""" + resource_class_id = fields.ResourceClass.index(inventory.resource_class) + inv_list = InventoryList(objects=[inventory]) + conn = context.session.connection() + with conn.begin(): + _update_inventory_for_provider( + conn, rp, inv_list, set([resource_class_id])) + rp.generation = _increment_provider_generation(conn, rp) + + +@db_api.api_context_manager.writer +def _delete_inventory(context, rp, resource_class_id): + """Delete up to one Inventory of the given resource_class id.""" + + conn = context.session.connection() + with conn.begin(): + if not _delete_inventory_from_provider(conn, rp, [resource_class_id]): + raise exception.NotFound( + 'No inventory of class %s found for delete' + % fields.ResourceClass.from_index(resource_class_id)) + rp.generation = _increment_provider_generation(conn, rp) + + @db_api.api_context_manager.writer def _set_inventory(context, rp, inv_list): """Given an InventoryList object, replaces the inventory of the @@ -238,11 +286,38 @@ class ResourceProvider(base.NovaObject): db_resource_provider = cls._get_by_uuid_from_db(context, uuid) return cls._from_db_object(context, cls(), db_resource_provider) + @base.remotable + def add_inventory(self, inventory): + """Add one new Inventory to the resource provider. + + Fails if Inventory of the provided resource class is + already present. + """ + _add_inventory(self._context, self, inventory) + self.obj_reset_changes() + + @base.remotable + def delete_inventory(self, resource_class): + """Delete Inventory of provided resource_class.""" + resource_class_id = fields.ResourceClass.index(resource_class) + _delete_inventory(self._context, self, resource_class_id) + self.obj_reset_changes() + @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) self.obj_reset_changes() + @base.remotable + def update_inventory(self, inventory): + """Update one existing Inventory of the same resource class. + + Fails if no Inventory of the same class is present. + """ + _update_inventory(self._context, self, inventory) + self.obj_reset_changes() + @staticmethod def _create_in_db(context, updates): return _create_rp_in_db(context, updates) @@ -345,6 +420,11 @@ class Inventory(_HasAResourceProvider): 'allocation_ratio': fields.NonNegativeFloatField(default=1.0), } + @property + def capacity(self): + """Inventory capacity, adjusted by allocation_ratio.""" + return int((self.total - self.reserved) * self.allocation_ratio) + @base.remotable def create(self): if 'id' in self: diff --git a/nova/tests/functional/db/test_resource_provider.py b/nova/tests/functional/db/test_resource_provider.py index 5fb7d1312e2c..cd57c802a4d4 100644 --- a/nova/tests/functional/db/test_resource_provider.py +++ b/nova/tests/functional/db/test_resource_provider.py @@ -10,6 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. + +from oslo_db import exception as db_exc + from nova import context from nova import exception from nova import objects @@ -137,7 +140,7 @@ class ResourceProviderTestCase(test.NoDBTestCase): self.context, resource_provider.uuid)) self.assertEqual(33, reloaded_inventories[0].total) - def test_provider_set_inventory(self): + def test_provider_modify_inventory(self): rp = objects.ResourceProvider(context=self.context, uuid=uuidsentinel.rp_uuid, name=uuidsentinel.rp_name) @@ -205,8 +208,7 @@ class ResourceProviderTestCase(test.NoDBTestCase): max_unit=100, step_size=10, allocation_ratio=1.0) - inv_list = objects.InventoryList(objects=[disk_inv]) - rp.set_inventory(inv_list) + rp.update_inventory(disk_inv) # generation has bumped self.assertEqual(saved_generation + 1, rp.generation) @@ -217,7 +219,80 @@ class ResourceProviderTestCase(test.NoDBTestCase): self.assertEqual(1, len(new_inv_list)) self.assertEqual(2048, new_inv_list[0].total) + # fail when inventory bad + disk_inv = objects.Inventory( + resource_provider=rp, + resource_class=fields.ResourceClass.DISK_GB, + total=2048, + reserved=2048) + disk_inv.obj_set_defaults() + self.assertRaises(exception.ObjectActionError, + rp.update_inventory, disk_inv) + + # generation has not bumped + self.assertEqual(saved_generation, rp.generation) + + # delete inventory + rp.delete_inventory(fields.ResourceClass.DISK_GB) + + # generation has bumped + self.assertEqual(saved_generation + 1, rp.generation) + saved_generation = rp.generation + + new_inv_list = objects.InventoryList.get_all_by_resource_provider_uuid( + self.context, uuidsentinel.rp_uuid) + result = new_inv_list.find(fields.ResourceClass.DISK_GB) + self.assertIsNone(result) + self.assertRaises(exception.NotFound, rp.delete_inventory, + fields.ResourceClass.DISK_GB) + + # check inventory list is empty + inv_list = objects.InventoryList.get_all_by_resource_provider_uuid( + self.context, uuidsentinel.rp_uuid) + self.assertEqual(0, len(inv_list)) + + # add some inventory + rp.add_inventory(vcpu_inv) + inv_list = objects.InventoryList.get_all_by_resource_provider_uuid( + self.context, uuidsentinel.rp_uuid) + self.assertEqual(1, len(inv_list)) + + # generation has bumped + self.assertEqual(saved_generation + 1, rp.generation) + saved_generation = rp.generation + + # add same inventory again + self.assertRaises(db_exc.DBDuplicateEntry, + rp.add_inventory, vcpu_inv) + + # generation has not bumped + self.assertEqual(saved_generation, rp.generation) + # fail when generation wrong rp.generation = rp.generation - 1 self.assertRaises(exception.ConcurrentUpdateDetected, rp.set_inventory, inv_list) + + def test_delete_inventory_not_found(self): + rp = objects.ResourceProvider(context=self.context, + uuid=uuidsentinel.rp_uuid, + name=uuidsentinel.rp_name) + rp.create() + error = self.assertRaises(exception.NotFound, rp.delete_inventory, + 'DISK_GB') + self.assertIn('No inventory of class DISK_GB found for delete', + str(error)) + + def test_update_inventory_not_found(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='DISK_GB', + total=2048) + disk_inv.obj_set_defaults() + error = self.assertRaises(exception.NotFound, rp.update_inventory, + disk_inv) + self.assertIn('No inventory of class DISK_GB found for update', + str(error)) diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index bc2a3c6b872d..645efd6b14fc 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -1180,7 +1180,7 @@ object_data = { 'Quotas': '1.2-1fe4cd50593aaf5d36a6dc5ab3f98fb3', 'QuotasNoOp': '1.2-e041ddeb7dc8188ca71706f78aad41c1', 'RequestSpec': '1.6-c1cb516acdf120d367a42d343ed695b5', - 'ResourceProvider': '1.0-94e0e906feb26a24e217935c1e401467', + 'ResourceProvider': '1.0-421c968ee9b2341dd78b0c19c904d4de', 'S3ImageMapping': '1.0-7dd7366a890d82660ed121de9092276e', 'SchedulerLimits': '1.0-249c4bd8e62a9b327b7026b7f19cc641', 'SchedulerRetries': '1.1-3c9c8b16143ebbb6ad7030e999d14cc0', diff --git a/nova/tests/unit/objects/test_resource_provider.py b/nova/tests/unit/objects/test_resource_provider.py index 5fae544b63f4..dc86400e0419 100644 --- a/nova/tests/unit/objects/test_resource_provider.py +++ b/nova/tests/unit/objects/test_resource_provider.py @@ -205,6 +205,22 @@ class _TestInventoryNoDB(object): self.assertEqual(1, inv.step_size) self.assertEqual(1.0, inv.allocation_ratio) + def test_capacity(self): + rp = objects.ResourceProvider(id=_RESOURCE_PROVIDER_ID, + uuid=_RESOURCE_PROVIDER_UUID) + kwargs = dict(resource_provider=rp, + resource_class=_RESOURCE_CLASS_NAME, + total=16, + reserved=16) + inv = objects.Inventory(self.context, **kwargs) + inv.obj_set_defaults() + + self.assertEqual(0, inv.capacity) + inv.reserved = 15 + self.assertEqual(1, inv.capacity) + inv.allocation_ratio = 2.0 + self.assertEqual(2, inv.capacity) + class TestInventoryNoDB(test_objects._LocalTest, _TestInventoryNoDB):