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:
parent
c72b55bee9
commit
3eaeda68ba
@ -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
|
||||
|
||||
|
||||
|
@ -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])
|
||||
|
@ -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"}'
|
||||
|
Loading…
Reference in New Issue
Block a user