Allow arbitrary image properties

In some circumstances, it is necessary to have
arbitrary image properties on Glance images.
An example is described here:
https://storyboard.openstack.org/#!/story/2008951

This patch adds the ability to specify those
properties using the WebImage resource.

Story: 2008951
Task: 42575
Change-Id: I23475185671c52b02eb57f1aa537f206b51c384a
This commit is contained in:
Brendan Shephard 2021-06-07 15:38:40 +10:00
parent c72b55bee9
commit 3eaeda68ba
3 changed files with 137 additions and 6 deletions

View File

@ -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

View File

@ -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])

View File

@ -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"}'