From d1c75df6793274fbbca92acbbde821c436a36c76 Mon Sep 17 00:00:00 2001 From: Jay Pipes Date: Mon, 31 Oct 2016 11:03:54 -0400 Subject: [PATCH] placement: adds ResourceClass.destroy() Adds ResourceClass.destroy() method to delete a custom resource class. Deletions of standard resource classes or resource classes that are referenced in an inventory record are forbidden. Change-Id: I7809fa8941338b3bcef4872c13a791992e221851 blueprint: custom-resource-classes --- nova/db/sqlalchemy/resource_class_cache.py | 24 ++++-- nova/exception.py | 8 ++ nova/objects/resource_provider.py | 30 ++++++++ .../functional/db/test_resource_provider.py | 74 +++++++++++++++++++ 4 files changed, 129 insertions(+), 7 deletions(-) diff --git a/nova/db/sqlalchemy/resource_class_cache.py b/nova/db/sqlalchemy/resource_class_cache.py index 9b78e66f0317..34737c000345 100644 --- a/nova/db/sqlalchemy/resource_class_cache.py +++ b/nova/db/sqlalchemy/resource_class_cache.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_concurrency import lockutils import six import sqlalchemy as sa @@ -19,6 +20,7 @@ from nova import exception from nova.objects import fields _RC_TBL = models.ResourceClass.__table__ +_LOCKNAME = 'rc_cache' def raise_if_custom_resource_class_pre_v1_1(rc): @@ -65,6 +67,11 @@ class ResourceClassCache(object): self.id_cache = {} self.str_cache = {} + def clear(self): + with lockutils.lock(_LOCKNAME): + self.id_cache = {} + self.str_cache = {} + def get_standards(self): """Return a list of {'id': , 'name': for all standard resource classes. @@ -89,13 +96,13 @@ class ResourceClassCache(object): :raises `exception.ResourceClassNotFound` if rc_str cannot be found in either the standard classes or the DB. """ - if rc_str in self.id_cache: - return self.id_cache[rc_str] - # First check the standard resource classes if rc_str in fields.ResourceClass.STANDARD: return fields.ResourceClass.STANDARD.index(rc_str) - else: + + with lockutils.lock(_LOCKNAME): + if rc_str in self.id_cache: + return self.id_cache[rc_str] # Otherwise, check the database table _refresh_from_db(self.ctx, self) if rc_str in self.id_cache: @@ -117,13 +124,16 @@ class ResourceClassCache(object): :raises `exception.ResourceClassNotFound` if rc_id cannot be found in either the standard classes or the DB. """ - if rc_id in self.str_cache: - return self.str_cache[rc_id] - # First check the fields.ResourceClass.STANDARD values try: return fields.ResourceClass.STANDARD[rc_id] except IndexError: + pass + + with lockutils.lock(_LOCKNAME): + if rc_id in self.str_cache: + return self.str_cache[rc_id] + # Otherwise, check the database table _refresh_from_db(self.ctx, self) if rc_id in self.str_cache: diff --git a/nova/exception.py b/nova/exception.py index d50b259a117a..64db0d74f4a3 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -2130,6 +2130,14 @@ class ResourceClassExists(NovaException): msg_fmt = _("Resource class %(resource_class)s already exists.") +class ResourceClassInUse(Invalid): + msg_fmt = _("Cannot delete resource class. Class is in use in inventory.") + + +class ResourceClassCannotDeleteStandard(Invalid): + msg_fmt = _("Cannot delete standard resource class %(resource_class)s.") + + class InvalidInventory(Invalid): msg_fmt = _("Inventory for '%(resource_class)s' on " "resource provider '%(resource_provider)s' invalid.") diff --git a/nova/objects/resource_provider.py b/nova/objects/resource_provider.py index 3d8f7ef03a63..617b98002635 100644 --- a/nova/objects/resource_provider.py +++ b/nova/objects/resource_provider.py @@ -1215,6 +1215,36 @@ class ResourceClass(base.NovaObject): context.session.add(rc) return rc + def destroy(self): + if 'id' not in self: + raise exception.ObjectActionError(action='destroy', + reason='ID attribute not found') + # Never delete any standard resource class, since the standard resource + # classes don't even exist in the database table anyway. + _ensure_rc_cache(self._context) + standards = _RC_CACHE.get_standards() + if self.id in (rc['id'] for rc in standards): + raise exception.ResourceClassCannotDeleteStandard( + resource_class=self.name) + + self._destroy(self._context, self.id) + _RC_CACHE.clear() + + @staticmethod + @db_api.api_context_manager.writer + def _destroy(context, _id): + # Don't delete the resource class if it is referred to in the + # inventories table. + num_inv = context.session.query(models.Inventory).filter( + models.Inventory.resource_class_id == _id).count() + if num_inv: + raise exception.ResourceClassInUse() + + res = context.session.query(models.ResourceClass).filter( + models.ResourceClass.id == _id).delete() + if not res: + raise exception.NotFound() + @base.NovaObjectRegistry.register class ResourceClassList(base.ObjectListBase, base.NovaObject): diff --git a/nova/tests/functional/db/test_resource_provider.py b/nova/tests/functional/db/test_resource_provider.py index 9ed0f32ba2c5..efc2510ef632 100644 --- a/nova/tests/functional/db/test_resource_provider.py +++ b/nova/tests/functional/db/test_resource_provider.py @@ -1148,3 +1148,77 @@ class ResourceClassTestCase(ResourceProviderBaseCase): name='CUSTOM_IRON_NFV', ) self.assertRaises(exception.ResourceClassExists, rc.create) + + def test_destroy_fail_no_id(self): + rc = objects.ResourceClass( + self.context, + name='CUSTOM_IRON_NFV', + ) + self.assertRaises(exception.ObjectActionError, rc.destroy) + + def test_destroy_fail_standard(self): + rc = objects.ResourceClass.get_by_name( + self.context, + 'VCPU', + ) + self.assertRaises(exception.ResourceClassCannotDeleteStandard, + rc.destroy) + + def test_destroy(self): + rc = objects.ResourceClass( + self.context, + name='CUSTOM_IRON_NFV', + ) + rc.create() + rc_list = objects.ResourceClassList.get_all(self.context) + rc_ids = (r.id for r in rc_list) + self.assertIn(rc.id, rc_ids) + + rc = objects.ResourceClass.get_by_name( + self.context, + 'CUSTOM_IRON_NFV', + ) + + rc.destroy() + rc_list = objects.ResourceClassList.get_all(self.context) + rc_ids = (r.id for r in rc_list) + self.assertNotIn(rc.id, rc_ids) + + # Verify rc cache was purged of the old entry + self.assertRaises(exception.ResourceClassNotFound, + objects.ResourceClass.get_by_name, + self.context, + 'CUSTOM_IRON_NFV') + + def test_destroy_fail_with_inventory(self): + """Test that we raise an exception when attempting to delete a resource + class that is referenced in an inventory record. + """ + rc = objects.ResourceClass( + self.context, + name='CUSTOM_IRON_NFV', + ) + rc.create() + rp = objects.ResourceProvider( + self.context, + name='my rp', + uuid=uuidsentinel.rp, + ) + rp.create() + inv = objects.Inventory( + resource_provider=rp, + resource_class='CUSTOM_IRON_NFV', + total=1, + ) + inv.obj_set_defaults() + inv_list = objects.InventoryList(objects=[inv]) + rp.set_inventory(inv_list) + + self.assertRaises(exception.ResourceClassInUse, + rc.destroy) + + rp.set_inventory(objects.InventoryList(objects=[])) + rc.destroy() + rc_list = objects.ResourceClassList.get_all(self.context) + rc_ids = (r.id for r in rc_list) + self.assertNotIn(rc.id, rc_ids)