diff --git a/nova/exception.py b/nova/exception.py index 728a5d84f9f..2870f155027 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -2137,3 +2137,15 @@ class InvalidEmulatorThreadsPolicy(Invalid): class BadRequirementEmulatorThreadsPolicy(Invalid): msg_fmt = _("An isolated CPU emulator threads option requires a dedicated " "CPU policy option.") + + +class TraitNotFound(NotFound): + msg_fmt = _("No such trait %(name)s.") + + +class TraitExists(NovaException): + msg_fmt = _("The Trait %(name)s already exists") + + +class TraitCannotDeleteStandard(Invalid): + msg_fmt = _("Cannot delete standard trait %(name)s.") diff --git a/nova/objects/resource_provider.py b/nova/objects/resource_provider.py index 8906c4eb845..152cea203fa 100644 --- a/nova/objects/resource_provider.py +++ b/nova/objects/resource_provider.py @@ -1454,3 +1454,121 @@ def get_all(cls, context): def __repr__(self): strings = [repr(x) for x in self.objects] return "ResourceClassList[" + ", ".join(strings) + "]" + + +@base.NovaObjectRegistry.register +class Trait(base.NovaObject): + # Version 1.0: Initial version + VERSION = '1.0' + + # All the user-defined traits must begin with this prefix. + CUSTOM_NAMESPACE = 'CUSTOM_' + + fields = { + 'id': fields.IntegerField(read_only=True), + 'name': fields.StringField(nullable=False) + } + + @staticmethod + def _from_db_object(context, trait, db_trait): + for key in trait.fields: + setattr(trait, key, db_trait[key]) + trait.obj_reset_changes() + trait._context = context + return trait + + @staticmethod + @db_api.api_context_manager.writer + def _create_in_db(context, updates): + trait = models.Trait() + trait.update(updates) + context.session.add(trait) + return trait + + def create(self): + if 'id' in self: + raise exception.ObjectActionError(action='create', + reason='already created') + if 'name' not in self: + raise exception.ObjectActionError(action='create', + reason='name is required') + + if not self.name.startswith(self.CUSTOM_NAMESPACE): + raise exception.ObjectActionError( + action='create', + reason='name must start with %s' % self.CUSTOM_NAMESPACE) + + updates = self.obj_get_changes() + + try: + db_trait = self._create_in_db(self._context, updates) + except db_exc.DBDuplicateEntry: + raise exception.TraitExists(name=self.name) + + self._from_db_object(self._context, self, db_trait) + + @staticmethod + @db_api.api_context_manager.reader + def _get_by_name_from_db(context, name): + result = context.session.query(models.Trait).filter_by( + name=name).first() + if not result: + raise exception.TraitNotFound(name=name) + return result + + @classmethod + def get_by_name(cls, context, name): + db_trait = cls._get_by_name_from_db(context, name) + return cls._from_db_object(context, cls(), db_trait) + + @staticmethod + @db_api.api_context_manager.writer + def _destroy_in_db(context, name): + res = context.session.query(models.Trait).filter_by( + name=name).delete() + if not res: + raise exception.TraitNotFound(name=name) + + def destroy(self): + if 'name' not in self: + raise exception.ObjectActionError(action='destroy', + reason='name is required') + + if not self.name.startswith(self.CUSTOM_NAMESPACE): + raise exception.TraitCannotDeleteStandard(name=self.name) + + if 'id' not in self: + raise exception.ObjectActionError(action='destroy', + reason='ID attribute not found') + + self._destroy_in_db(self._context, self.name) + + +@base.NovaObjectRegistry.register +class TraitList(base.ObjectListBase, base.NovaObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'objects': fields.ListOfObjectsField('Trait') + } + + @staticmethod + @db_api.api_context_manager.reader + def _get_all_from_db(context, filters): + if not filters: + filters = {} + + query = context.session.query(models.Trait) + if 'name_in' in filters: + query = query.filter(models.Trait.name.in_(filters['name_in'])) + if 'prefix' in filters: + query = query.filter( + models.Trait.name.like(filters['prefix'] + '%')) + + return query.all() + + @base.remotable_classmethod + def get_all(cls, context, filters=None): + db_traits = cls._get_all_from_db(context, filters) + return base.obj_make_list(context, cls(context), Trait, db_traits) diff --git a/nova/tests/functional/db/test_resource_provider.py b/nova/tests/functional/db/test_resource_provider.py index 1d053b97ffe..d9fd85412aa 100644 --- a/nova/tests/functional/db/test_resource_provider.py +++ b/nova/tests/functional/db/test_resource_provider.py @@ -1533,3 +1533,117 @@ def test_save(self): objects.ResourceClass.get_by_name, self.context, 'CUSTOM_IRON_NFV') + + +class ResourceProviderTraitsTestCase(ResourceProviderBaseCase): + + def _assert_traits(self, expected_traits, traits_objs): + expected_traits.sort() + traits = [] + for obj in traits_objs: + traits.append(obj.name) + traits.sort() + self.assertEqual(expected_traits, traits) + + def test_trait_create(self): + t = objects.Trait(self.context) + t.name = 'CUSTOM_TRAIT_A' + t.create() + self.assertIn('id', t) + self.assertEqual(t.name, 'CUSTOM_TRAIT_A') + + def test_trait_create_with_id_set(self): + t = objects.Trait(self.context) + t.name = 'CUSTOM_TRAIT_A' + t.id = 1 + self.assertRaises(exception.ObjectActionError, t.create) + + def test_trait_create_without_name_set(self): + t = objects.Trait(self.context) + self.assertRaises(exception.ObjectActionError, t.create) + + def test_trait_create_without_custom_prefix(self): + t = objects.Trait(self.context) + t.name = 'TRAIT_A' + self.assertRaises(exception.ObjectActionError, t.create) + + def test_trait_create_duplicated_trait(self): + trait = objects.Trait(self.context) + trait.name = 'CUSTOM_TRAIT_A' + trait.create() + tmp_trait = objects.Trait.get_by_name(self.context, 'CUSTOM_TRAIT_A') + self.assertEqual('CUSTOM_TRAIT_A', tmp_trait.name) + duplicated_trait = objects.Trait(self.context) + duplicated_trait.name = 'CUSTOM_TRAIT_A' + self.assertRaises(exception.TraitExists, duplicated_trait.create) + + def test_trait_get(self): + t = objects.Trait(self.context) + t.name = 'CUSTOM_TRAIT_A' + t.create() + t = objects.Trait.get_by_name(self.context, 'CUSTOM_TRAIT_A') + self.assertEqual(t.name, 'CUSTOM_TRAIT_A') + + def test_trait_get_non_existed_trait(self): + self.assertRaises(exception.TraitNotFound, + objects.Trait.get_by_name, self.context, 'CUSTOM_TRAIT_A') + + def test_trait_destroy(self): + t = objects.Trait(self.context) + t.name = 'CUSTOM_TRAIT_A' + t.create() + t = objects.Trait.get_by_name(self.context, 'CUSTOM_TRAIT_A') + self.assertEqual(t.name, 'CUSTOM_TRAIT_A') + t.destroy() + self.assertRaises(exception.TraitNotFound, objects.Trait.get_by_name, + self.context, 'CUSTOM_TRAIT_A') + + def test_trait_destroy_with_standard_trait(self): + t = objects.Trait(self.context) + t.id = 1 + t.name = 'HW_CPU_X86_AVX' + self.assertRaises(exception.TraitCannotDeleteStandard, t.destroy) + + def test_traits_get_all(self): + trait_names = ['CUSTOM_TRAIT_A', 'CUSTOM_TRAIT_B', 'CUSTOM_TRAIT_C'] + for name in trait_names: + t = objects.Trait(self.context) + t.name = name + t.create() + + self._assert_traits(trait_names, + objects.TraitList.get_all(self.context)) + + def test_traits_get_all_with_name_in_filter(self): + trait_names = ['CUSTOM_TRAIT_A', 'CUSTOM_TRAIT_B', 'CUSTOM_TRAIT_C'] + for name in trait_names: + t = objects.Trait(self.context) + t.name = name + t.create() + + traits = objects.TraitList.get_all(self.context, + filters={'name_in': ['CUSTOM_TRAIT_A', 'CUSTOM_TRAIT_B']}) + self._assert_traits(['CUSTOM_TRAIT_A', 'CUSTOM_TRAIT_B'], traits) + + def test_traits_get_all_with_non_existed_name(self): + traits = objects.TraitList.get_all(self.context, + filters={'name_in': ['CUSTOM_TRAIT_X', 'CUSTOM_TRAIT_Y']}) + self.assertEqual(0, len(traits)) + + def test_traits_get_all_with_prefix_filter(self): + trait_names = ['CUSTOM_TRAIT_A', 'CUSTOM_TRAIT_B', 'CUSTOM_TRAIT_C'] + for name in trait_names: + t = objects.Trait(self.context) + t.name = name + t.create() + + traits = objects.TraitList.get_all(self.context, + filters={'prefix': 'CUSTOM'}) + self._assert_traits( + ['CUSTOM_TRAIT_A', 'CUSTOM_TRAIT_B', 'CUSTOM_TRAIT_C'], + traits) + + def test_traits_get_all_with_non_existed_prefix(self): + traits = objects.TraitList.get_all(self.context, + filters={"prefix": "NOT_EXISTED"}) + self.assertEqual(0, len(traits)) diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index 6fd37a134ef..d62c3629b60 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -1163,6 +1163,8 @@ def obj_name(cls): 'TaskLogList': '1.0-cc8cce1af8a283b9d28b55fcd682e777', 'Tag': '1.1-8b8d7d5b48887651a0e01241672e2963', 'TagList': '1.1-55231bdb671ecf7641d6a2e9109b5d8e', + 'Trait': '1.0-2b58dd7c5037153cb4bfc94c0ae5dd3a', + 'TraitList': '1.0-ff48fc1575f20800796b48266114c608', 'Usage': '1.1-b738dbebeb20e3199fc0ebca6e292a47', 'UsageList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e', 'USBDeviceBus': '1.0-e4c7dd6032e46cd74b027df5eb2d4750',