summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2021-08-25 04:05:03 +0000
committerGerrit Code Review <review@openstack.org>2021-08-25 04:05:03 +0000
commitff11d5e9bde78bbcbdb7beb5e42d49f297e5ff01 (patch)
tree7dbbcaaa051b5af70389cd5c8041ceac36f603a9
parent42b5f68bf047252a7ec6e0db98f0546c042286f3 (diff)
parent3eaeda68bad45d57588f1600518a1b6794b076b2 (diff)
downloadheat-ff11d5e9bde78bbcbdb7beb5e42d49f297e5ff01.tar.gz
Merge "Allow arbitrary image properties"
-rw-r--r--heat/engine/resources/openstack/glance/image.py42
-rw-r--r--heat/tests/openstack/glance/test_image.py91
-rw-r--r--releasenotes/notes/add-extra-properties-to-glance-bb6e6e5d02473877.yaml10
3 files changed, 137 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])
diff --git a/releasenotes/notes/add-extra-properties-to-glance-bb6e6e5d02473877.yaml b/releasenotes/notes/add-extra-properties-to-glance-bb6e6e5d02473877.yaml
new file mode 100644
index 000000000..3a022926d
--- /dev/null
+++ b/releasenotes/notes/add-extra-properties-to-glance-bb6e6e5d02473877.yaml
@@ -0,0 +1,10 @@
+---
+prelude: >
+ Add the ability to specify extra_properties for Glance images. This is useful
+ for example when using secure boot and are required to have specific properties
+ defined on the Glance images.
+features:
+ - |
+ extra_properties key added to the OS::Glance::WebImage type. This parameter
+ takes a map value such as '{"hw_firmware_type": "uefi", "os_secure_boot": "required"}'
+