diff options
author | Zuul <zuul@review.opendev.org> | 2021-08-25 04:05:03 +0000 |
---|---|---|
committer | Gerrit Code Review <review@openstack.org> | 2021-08-25 04:05:03 +0000 |
commit | ff11d5e9bde78bbcbdb7beb5e42d49f297e5ff01 (patch) | |
tree | 7dbbcaaa051b5af70389cd5c8041ceac36f603a9 /heat | |
parent | 42b5f68bf047252a7ec6e0db98f0546c042286f3 (diff) | |
parent | 3eaeda68bad45d57588f1600518a1b6794b076b2 (diff) | |
download | heat-ff11d5e9bde78bbcbdb7beb5e42d49f297e5ff01.tar.gz |
Merge "Allow arbitrary image properties"
Diffstat (limited to 'heat')
-rw-r--r-- | heat/engine/resources/openstack/glance/image.py | 42 | ||||
-rw-r--r-- | heat/tests/openstack/glance/test_image.py | 91 |
2 files changed, 127 insertions, 6 deletions
diff --git a/heat/engine/resources/openstack/glance/image.py b/heat/engine/resources/openstack/glance/image.py index aee760142..74d834523 100644 --- a/heat/engine/resources/openstack/glance/image.py +++ b/heat/engine/resources/openstack/glance/image.py @@ -31,12 +31,13 @@ class GlanceWebImage(resource.Resource): NAME, IMAGE_ID, MIN_DISK, MIN_RAM, PROTECTED, DISK_FORMAT, CONTAINER_FORMAT, LOCATION, TAGS, ARCHITECTURE, KERNEL_ID, OS_DISTRO, OS_VERSION, OWNER, - VISIBILITY, RAMDISK_ID, ACTIVE, MEMBERS + EXTRA_PROPERTIES, VISIBILITY, RAMDISK_ID, ACTIVE, MEMBERS ) = ( 'name', 'id', 'min_disk', 'min_ram', 'protected', 'disk_format', 'container_format', 'location', 'tags', 'architecture', 'kernel_id', 'os_distro', 'os_version', - 'owner', 'visibility', 'ramdisk_id', 'active', 'members' + 'owner', 'extra_properties', 'visibility', 'ramdisk_id', + 'active', 'members' ) glance_id_pattern = ('^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}' @@ -139,6 +140,13 @@ class GlanceWebImage(resource.Resource): _('Owner of the image.'), update_allowed=True, ), + EXTRA_PROPERTIES: properties.Schema( + properties.Schema.MAP, + _('Arbitrary properties to associate with the image.'), + update_allowed=True, + default={}, + support_status=support.SupportStatus(version='17.0.0') + ), VISIBILITY: properties.Schema( properties.Schema.STRING, _('Scope of image accessibility.'), @@ -188,7 +196,7 @@ class GlanceWebImage(resource.Resource): def handle_create(self): args = dict((k, v) for k, v in self.properties.items() - if v is not None) + if v is not None and k is not self.EXTRA_PROPERTIES) members = args.pop(self.MEMBERS, []) active = args.pop(self.ACTIVE) location = args.pop(self.LOCATION) @@ -199,6 +207,8 @@ class GlanceWebImage(resource.Resource): images.image_import(image_id, method='web-download', uri=location) for member in members: self.client().image_members.create(image_id, member) + props = self.properties.get(self.EXTRA_PROPERTIES) + images.update(image.id, **props) return active def check_create_complete(self, active): @@ -219,10 +229,11 @@ class GlanceWebImage(resource.Resource): return image.status == 'active' def handle_update(self, json_snippet, tmpl_diff, prop_diff): + images = self.client().images if prop_diff: active = prop_diff.pop(self.ACTIVE, None) if active is False: - self.client().images.deactivate(self.resource_id) + images.deactivate(self.resource_id) if self.TAGS in prop_diff: existing_tags = self.properties.get(self.TAGS) or [] @@ -241,6 +252,19 @@ class GlanceWebImage(resource.Resource): self.resource_id, tag) + if self.EXTRA_PROPERTIES in prop_diff: + old_properties = self.properties.get(self.EXTRA_PROPERTIES) + new_properties = prop_diff.pop(self.EXTRA_PROPERTIES) + prop_diff.update(new_properties) + remove_props = list(set(old_properties) - set(new_properties)) + + # Though remove_props defaults to None within the glanceclient, + # setting it to a list (possibly []) every time ensures only one + # calling format to images.update + images.update(self.resource_id, remove_props, **prop_diff) + else: + images.update(self.resource_id, **prop_diff) + if self.MEMBERS in prop_diff: existing_members = self.properties.get(self.MEMBERS) or [] diff_members = prop_diff.pop(self.MEMBERS) or [] @@ -254,7 +278,6 @@ class GlanceWebImage(resource.Resource): self.glance().image_members.delete( self.resource_id, _member) - self.client().images.update(self.resource_id, **prop_diff) return active def check_update_complete(self, active): @@ -300,9 +323,18 @@ class GlanceWebImage(resource.Resource): self.IMAGE_ID)}) else: image_reality.update({self.IMAGE_ID: None}) + + if key == self.EXTRA_PROPERTIES: + continue else: image_reality.update({key: resource_data.get(key)}) + if resource_properties.get(self.EXTRA_PROPERTIES): + extra_properties = {} + for key in resource_properties.get(self.EXTRA_PROPERTIES): + extra_properties[key] = resource_data.get(key) + image_reality.update({self.EXTRA_PROPERTIES: extra_properties}) + return image_reality diff --git a/heat/tests/openstack/glance/test_image.py b/heat/tests/openstack/glance/test_image.py index 3f4a8c6d1..275bad39a 100644 --- a/heat/tests/openstack/glance/test_image.py +++ b/heat/tests/openstack/glance/test_image.py @@ -479,6 +479,7 @@ class GlanceWebImageTest(common.HeatTestCase): self.images = self.glanceclient.images self.image_tags = self.glanceclient.image_tags self.image_members = self.glanceclient.image_members + self.update = self.glanceclient.update def _test_validate(self, resource, error_msg): exc = self.assertRaises(exception.StackValidationFailed, @@ -622,6 +623,7 @@ class GlanceWebImageTest(common.HeatTestCase): self.image_tags.update.return_value = None props = self.stack.t.t['resources']['my_image']['properties'].copy() props['tags'] = ['tag1'] + props['extra_properties'] = {"hw_firmware_type": "uefi"} self.my_image.t = self.my_image.t.freeze(properties=props) self.my_image.reparse() self.my_image.handle_create() @@ -644,6 +646,8 @@ class GlanceWebImageTest(common.HeatTestCase): owner=u'test_owner', tags=['tag1'] ) + self.images.update.assert_called_once_with( + image_id, hw_firmware_type='uefi') def test_image_active_property_image_not_active(self): self.images.reactivate.return_value = None @@ -685,6 +689,16 @@ class GlanceWebImageTest(common.HeatTestCase): self.my_image.check_create_complete, False) self.assertIn('killed', ex.message) + def _handle_update_image_props(self, prop_diff): + self.my_image.handle_update(json_snippet=None, + tmpl_diff=None, + prop_diff=prop_diff) + self.images.update.assert_called_once_with( + self.my_image.resource_id, + ['hw_firmware_type'], + os_secure_boot='required' + ) + def _handle_update_tags(self, prop_diff): self.my_image.handle_update(json_snippet=None, tmpl_diff=None, @@ -711,7 +725,7 @@ class GlanceWebImageTest(common.HeatTestCase): self.my_image.handle_update(json_snippet=None, tmpl_diff=None, prop_diff=prop_diff) - self.images.update.assert_called_once_with( + self.images.update.assert_called_with( self.my_image.resource_id, architecture='test_architecture', kernel_id='12345678-1234-1234-1234-123456789012', @@ -763,6 +777,17 @@ class GlanceWebImageTest(common.HeatTestCase): self.images.reactivate.assert_called_once_with( self.my_image.resource_id) + def test_image_handle_update_image_props(self): + self.my_image.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151' + + props = self.stack.t.t['resources']['my_image']['properties'].copy() + props['extra_properties'] = {"hw_firmware_type": "uefi"} + self.my_image.t = self.my_image.t.freeze(properties=props) + self.my_image.reparse() + prop_diff = {'extra_properties': {"os_secure_boot": "required"}} + + self._handle_update_image_props(prop_diff) + def test_image_handle_update_tags(self): self.my_image.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151' @@ -929,3 +954,67 @@ class GlanceWebImageTest(common.HeatTestCase): self.assertRaises(exception.EntityNotFound, self.my_image.get_live_state, self.my_image.properties) + + def test_parse_live_resource_data(self): + resource_data = { + 'name': 'test', + 'disk_format': 'qcow2', + 'container_format': 'bare', + 'active': None, + 'protected': False, + 'is_public': False, + 'min_disk': 0, + 'min_ram': 0, + 'id': '41f0e60c-ebb4-4375-a2b4-845ae8b9c995', + 'tags': [], + 'architecture': 'test_architecture', + 'kernel_id': '12345678-1234-1234-1234-123456789012', + 'os_distro': 'new_distro', + 'os_version': '1.0', + 'os_secure_boot': 'False', + 'owner': 'new_owner', + 'hw_firmware_type': 'uefi', + 'ramdisk_id': '12345678-1234-1234-1234-123456789012', + 'members': None, + 'visibility': 'private' + } + + resource_properties = self.stack.t.t['resources'][ + 'my_image']['properties'].copy() + resource_properties['extra_properties'] = { + 'hw_firmware_type': 'uefi', + 'os_secure_boot': 'required', + } + + reality = self.my_image.parse_live_resource_data(resource_properties, + resource_data) + expected = { + 'name': 'test', + 'disk_format': 'qcow2', + 'container_format': 'bare', + 'active': None, + 'protected': False, + 'min_disk': 0, + 'min_ram': 0, + 'id': '41f0e60c-ebb4-4375-a2b4-845ae8b9c995', + 'tags': [], + 'architecture': 'test_architecture', + 'kernel_id': '12345678-1234-1234-1234-123456789012', + 'os_distro': 'new_distro', + 'os_version': '1.0', + 'owner': 'new_owner', + 'ramdisk_id': '12345678-1234-1234-1234-123456789012', + 'members': None, + 'visibility': 'private', + 'extra_properties': { + 'hw_firmware_type': 'uefi', + 'os_secure_boot': 'False', + } + } + + self.assertEqual(set(expected.keys()), set(reality.keys())) + for key in expected: + self.assertEqual(expected[key], reality[key]) + for key in expected['extra_properties']: + self.assertEqual(expected['extra_properties'][key], + reality['extra_properties'][key]) |