From 124a7d8cc6a9b0495ac4aa5b8e208b6613de74cd Mon Sep 17 00:00:00 2001 From: arvindn05 Date: Tue, 27 Feb 2018 21:56:31 -0800 Subject: [PATCH] Update ImageMetaProp object to expose traits The image meta data object now parses the un-numbered traits fields defined as part of image properties. These traits are intended to be passed to the scheduler -> placement api to pick the resources matching the traits. Added unit tests for the same. Change-Id: I86872e482d9a646204e4c3a3e5126fea474b574b Implements: blueprint glance-image-traits --- nova/objects/image_meta.py | 30 ++++++++++++-- nova/tests/unit/objects/test_image_meta.py | 47 ++++++++++++++++++++++ nova/tests/unit/objects/test_objects.py | 2 +- 3 files changed, 75 insertions(+), 4 deletions(-) diff --git a/nova/objects/image_meta.py b/nova/objects/image_meta.py index 3beb5007b658..c098e3b56b6a 100644 --- a/nova/objects/image_meta.py +++ b/nova/objects/image_meta.py @@ -15,6 +15,7 @@ import copy from oslo_utils import versionutils +import six from nova import exception from nova import objects @@ -168,12 +169,15 @@ class ImageMetaProps(base.NovaObject): # Version 1.17: Add lan9118 as valid nic for hw_vif_model property for qemu # Version 1.18: Pull signature properties from cursive library # Version 1.19: Added 'img_hide_hypervisor_id' type field - VERSION = '1.19' + # Version 1.20: Added 'traits_required' list field + VERSION = '1.20' def obj_make_compatible(self, primitive, target_version): super(ImageMetaProps, self).obj_make_compatible(primitive, target_version) target_version = versionutils.convert_version_to_tuple(target_version) + if target_version < (1, 20): + primitive.pop('traits_required', None) if target_version < (1, 19): primitive.pop('img_hide_hypervisor_id', None) if target_version < (1, 16) and 'hw_watchdog_action' in primitive: @@ -221,6 +225,7 @@ class ImageMetaProps(base.NovaObject): # 'hw_' - settings affecting the guest virtual machine hardware # 'img_' - settings affecting the use of images by the compute node # 'os_' - settings affecting the guest operating system setup + # 'traits_required' - The required traits associated with the image fields = { # name of guest hardware architecture eg i686, x86_64, ppc64 @@ -457,6 +462,13 @@ class ImageMetaProps(base.NovaObject): # is a fairly generic type. For a detailed type consider os_distro # instead 'os_type': fields.OSTypeField(), + + # The required traits associated with the image. Traits are expected to + # be defined as starting with `trait:` like below: + # trait:HW_CPU_X86_AVX2=required + # for trait in image_meta.traits_required: + # will yield trait strings such as 'HW_CPU_X86_AVX2' + 'traits_required': fields.ListOfStringsField(), } # The keys are the legacy property names and @@ -543,16 +555,26 @@ class ImageMetaProps(base.NovaObject): elif key == "hw_numa_cpus": self._set_numa_cpus(image_props) else: - if key not in image_props: + # traits_required will be populated by + # _set_attr_from_trait_names + if key not in image_props or key == "traits_required": continue setattr(self, key, image_props[key]) + def _set_attr_from_trait_names(self, image_props): + for trait in [six.text_type(k[6:]) for k, v in image_props.items() + if six.text_type(k).startswith("trait:") + and six.text_type(v) == six.text_type('required')]: + if 'traits_required' not in self: + self.traits_required = [] + self.traits_required.append(trait) + @classmethod def from_dict(cls, image_props): """Create instance from image properties dict - :param image_props: dictionary of image metdata properties + :param image_props: dictionary of image metadata properties Creates a new object instance, initializing from a dictionary of image metadata properties @@ -567,6 +589,8 @@ class ImageMetaProps(base.NovaObject): # associated with the current name takes priority obj._set_attr_from_legacy_names(image_props) obj._set_attr_from_current_names(image_props) + obj._set_attr_from_trait_names(image_props) + return obj def get(self, name, defvalue=None): diff --git a/nova/tests/unit/objects/test_image_meta.py b/nova/tests/unit/objects/test_image_meta.py index 1e3ca5eb0c26..e85641c7d0e3 100644 --- a/nova/tests/unit/objects/test_image_meta.py +++ b/nova/tests/unit/objects/test_image_meta.py @@ -108,6 +108,7 @@ class TestImageMetaProps(test.NoDBTestCase): 'hw_video_model': 'vga', 'hw_video_ram': '512', 'hw_qemu_guest_agent': 'yes', + 'trait:CUSTOM_TRUSTED': 'required', # Fill sane values for the rest here } virtprops = objects.ImageMetaProps.from_dict(props) @@ -115,6 +116,8 @@ class TestImageMetaProps(test.NoDBTestCase): self.assertEqual('vga', virtprops.hw_video_model) self.assertEqual(512, virtprops.hw_video_ram) self.assertTrue(virtprops.hw_qemu_guest_agent) + self.assertIsNotNone(virtprops.traits_required) + self.assertIn('CUSTOM_TRUSTED', virtprops.traits_required) def test_default_props(self): props = {} @@ -128,6 +131,7 @@ class TestImageMetaProps(test.NoDBTestCase): virtprops = objects.ImageMetaProps.from_dict(props) self.assertEqual("hvm", virtprops.get("hw_vm_mode", "hvm")) + self.assertIsNone(virtprops.get("traits_required")) def test_non_existent_prop(self): props = {} @@ -281,6 +285,43 @@ class TestImageMetaProps(test.NoDBTestCase): self.assertEqual([set([0, 1, 2, 3])], virtprops.hw_numa_cpus) + def test_get_unnumbered_trait_fields(self): + """Tests that only valid un-numbered required traits are parsed from + the properties. + """ + props = {'trait:HW_CPU_X86_AVX2': 'required', + 'trait:CUSTOM_TRUSTED': 'required', + 'trait1:CUSTOM_FPGA': 'required', + 'trai:CUSTOM_FOO': 'required', + 'trait:CUSTOM_XYZ': 'xyz'} + + virtprops = objects.ImageMetaProps.from_dict(props) + + self.assertIn('CUSTOM_TRUSTED', virtprops.traits_required) + self.assertIn('HW_CPU_X86_AVX2', virtprops.traits_required) + + # numbered traits are ignored + self.assertNotIn('CUSTOM_FPGA', virtprops.traits_required) + # property key does not start with `trait:` exactly + self.assertNotIn('CUSTOM_FOO', virtprops.traits_required) + # property value is not required + self.assertNotIn('CUSTOM_XYZ', virtprops.traits_required) + + def test_traits_required_initialized_as_list(self): + """Tests that traits_required field is set as a list even if the same + property is set on the image metadata. + """ + props = {'trait:HW_CPU_X86_AVX2': 'required', + 'trait:CUSTOM_TRUSTED': 'required', + 'traits_required': 'foo'} + + virtprops = objects.ImageMetaProps.from_dict(props) + + self.assertIsInstance(virtprops.traits_required, list) + self.assertIn('CUSTOM_TRUSTED', virtprops.traits_required) + self.assertIn('HW_CPU_X86_AVX2', virtprops.traits_required) + self.assertEqual(2, len(virtprops.traits_required)) + def test_obj_make_compatible(self): props = { 'hw_firmware_type': 'uefi', @@ -330,3 +371,9 @@ class TestImageMetaProps(test.NoDBTestCase): primitive = obj.obj_to_primitive('1.0') self.assertNotIn('img_hide_hypervisor_id', primitive['nova_object.data']) + + def test_obj_make_compatible_trait_fields(self): + """Tests that checks if we pop traits_required.""" + obj = objects.ImageMetaProps(traits_required=['CUSTOM_TRUSTED']) + primitive = obj.obj_to_primitive('1.19') + self.assertNotIn('traits_required', primitive['nova_object.data']) diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index ca691f5de64a..f0fc5673abed 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -1094,7 +1094,7 @@ object_data = { 'HVSpec': '1.2-de06bcec472a2f04966b855a49c46b41', 'IDEDeviceBus': '1.0-29d4c9f27ac44197f01b6ac1b7e16502', 'ImageMeta': '1.8-642d1b2eb3e880a367f37d72dd76162d', - 'ImageMetaProps': '1.19-dc9581ff2b80d8c33462889916b82df0', + 'ImageMetaProps': '1.20-ffd686cde289814695d5f89522aa5aef', 'Instance': '2.4-4437eb8b2737c3054ea579b8efe31dc5', 'InstanceAction': '1.1-f9f293e526b66fca0d05c3b3a2d13914', 'InstanceActionEvent': '1.1-e56a64fa4710e43ef7af2ad9d6028b33',