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,
|
NAME, IMAGE_ID, MIN_DISK, MIN_RAM, PROTECTED,
|
||||||
DISK_FORMAT, CONTAINER_FORMAT, LOCATION, TAGS,
|
DISK_FORMAT, CONTAINER_FORMAT, LOCATION, TAGS,
|
||||||
ARCHITECTURE, KERNEL_ID, OS_DISTRO, OS_VERSION, OWNER,
|
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',
|
'name', 'id', 'min_disk', 'min_ram', 'protected',
|
||||||
'disk_format', 'container_format', 'location', 'tags',
|
'disk_format', 'container_format', 'location', 'tags',
|
||||||
'architecture', 'kernel_id', 'os_distro', 'os_version',
|
'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}'
|
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.'),
|
_('Owner of the image.'),
|
||||||
update_allowed=True,
|
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(
|
VISIBILITY: properties.Schema(
|
||||||
properties.Schema.STRING,
|
properties.Schema.STRING,
|
||||||
_('Scope of image accessibility.'),
|
_('Scope of image accessibility.'),
|
||||||
@ -188,7 +196,7 @@ class GlanceWebImage(resource.Resource):
|
|||||||
|
|
||||||
def handle_create(self):
|
def handle_create(self):
|
||||||
args = dict((k, v) for k, v in self.properties.items()
|
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, [])
|
members = args.pop(self.MEMBERS, [])
|
||||||
active = args.pop(self.ACTIVE)
|
active = args.pop(self.ACTIVE)
|
||||||
location = args.pop(self.LOCATION)
|
location = args.pop(self.LOCATION)
|
||||||
@ -199,6 +207,8 @@ class GlanceWebImage(resource.Resource):
|
|||||||
images.image_import(image_id, method='web-download', uri=location)
|
images.image_import(image_id, method='web-download', uri=location)
|
||||||
for member in members:
|
for member in members:
|
||||||
self.client().image_members.create(image_id, member)
|
self.client().image_members.create(image_id, member)
|
||||||
|
props = self.properties.get(self.EXTRA_PROPERTIES)
|
||||||
|
images.update(image.id, **props)
|
||||||
return active
|
return active
|
||||||
|
|
||||||
def check_create_complete(self, active):
|
def check_create_complete(self, active):
|
||||||
@ -219,10 +229,11 @@ class GlanceWebImage(resource.Resource):
|
|||||||
return image.status == 'active'
|
return image.status == 'active'
|
||||||
|
|
||||||
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
|
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
|
||||||
|
images = self.client().images
|
||||||
if prop_diff:
|
if prop_diff:
|
||||||
active = prop_diff.pop(self.ACTIVE, None)
|
active = prop_diff.pop(self.ACTIVE, None)
|
||||||
if active is False:
|
if active is False:
|
||||||
self.client().images.deactivate(self.resource_id)
|
images.deactivate(self.resource_id)
|
||||||
|
|
||||||
if self.TAGS in prop_diff:
|
if self.TAGS in prop_diff:
|
||||||
existing_tags = self.properties.get(self.TAGS) or []
|
existing_tags = self.properties.get(self.TAGS) or []
|
||||||
@ -241,6 +252,19 @@ class GlanceWebImage(resource.Resource):
|
|||||||
self.resource_id,
|
self.resource_id,
|
||||||
tag)
|
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:
|
if self.MEMBERS in prop_diff:
|
||||||
existing_members = self.properties.get(self.MEMBERS) or []
|
existing_members = self.properties.get(self.MEMBERS) or []
|
||||||
diff_members = prop_diff.pop(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.glance().image_members.delete(
|
||||||
self.resource_id, _member)
|
self.resource_id, _member)
|
||||||
|
|
||||||
self.client().images.update(self.resource_id, **prop_diff)
|
|
||||||
return active
|
return active
|
||||||
|
|
||||||
def check_update_complete(self, active):
|
def check_update_complete(self, active):
|
||||||
@ -300,9 +323,18 @@ class GlanceWebImage(resource.Resource):
|
|||||||
self.IMAGE_ID)})
|
self.IMAGE_ID)})
|
||||||
else:
|
else:
|
||||||
image_reality.update({self.IMAGE_ID: None})
|
image_reality.update({self.IMAGE_ID: None})
|
||||||
|
|
||||||
|
if key == self.EXTRA_PROPERTIES:
|
||||||
|
continue
|
||||||
else:
|
else:
|
||||||
image_reality.update({key: resource_data.get(key)})
|
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
|
return image_reality
|
||||||
|
|
||||||
|
|
||||||
|
@ -479,6 +479,7 @@ class GlanceWebImageTest(common.HeatTestCase):
|
|||||||
self.images = self.glanceclient.images
|
self.images = self.glanceclient.images
|
||||||
self.image_tags = self.glanceclient.image_tags
|
self.image_tags = self.glanceclient.image_tags
|
||||||
self.image_members = self.glanceclient.image_members
|
self.image_members = self.glanceclient.image_members
|
||||||
|
self.update = self.glanceclient.update
|
||||||
|
|
||||||
def _test_validate(self, resource, error_msg):
|
def _test_validate(self, resource, error_msg):
|
||||||
exc = self.assertRaises(exception.StackValidationFailed,
|
exc = self.assertRaises(exception.StackValidationFailed,
|
||||||
@ -622,6 +623,7 @@ class GlanceWebImageTest(common.HeatTestCase):
|
|||||||
self.image_tags.update.return_value = None
|
self.image_tags.update.return_value = None
|
||||||
props = self.stack.t.t['resources']['my_image']['properties'].copy()
|
props = self.stack.t.t['resources']['my_image']['properties'].copy()
|
||||||
props['tags'] = ['tag1']
|
props['tags'] = ['tag1']
|
||||||
|
props['extra_properties'] = {"hw_firmware_type": "uefi"}
|
||||||
self.my_image.t = self.my_image.t.freeze(properties=props)
|
self.my_image.t = self.my_image.t.freeze(properties=props)
|
||||||
self.my_image.reparse()
|
self.my_image.reparse()
|
||||||
self.my_image.handle_create()
|
self.my_image.handle_create()
|
||||||
@ -644,6 +646,8 @@ class GlanceWebImageTest(common.HeatTestCase):
|
|||||||
owner=u'test_owner',
|
owner=u'test_owner',
|
||||||
tags=['tag1']
|
tags=['tag1']
|
||||||
)
|
)
|
||||||
|
self.images.update.assert_called_once_with(
|
||||||
|
image_id, hw_firmware_type='uefi')
|
||||||
|
|
||||||
def test_image_active_property_image_not_active(self):
|
def test_image_active_property_image_not_active(self):
|
||||||
self.images.reactivate.return_value = None
|
self.images.reactivate.return_value = None
|
||||||
@ -685,6 +689,16 @@ class GlanceWebImageTest(common.HeatTestCase):
|
|||||||
self.my_image.check_create_complete, False)
|
self.my_image.check_create_complete, False)
|
||||||
self.assertIn('killed', ex.message)
|
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):
|
def _handle_update_tags(self, prop_diff):
|
||||||
self.my_image.handle_update(json_snippet=None,
|
self.my_image.handle_update(json_snippet=None,
|
||||||
tmpl_diff=None,
|
tmpl_diff=None,
|
||||||
@ -711,7 +725,7 @@ class GlanceWebImageTest(common.HeatTestCase):
|
|||||||
self.my_image.handle_update(json_snippet=None,
|
self.my_image.handle_update(json_snippet=None,
|
||||||
tmpl_diff=None,
|
tmpl_diff=None,
|
||||||
prop_diff=prop_diff)
|
prop_diff=prop_diff)
|
||||||
self.images.update.assert_called_once_with(
|
self.images.update.assert_called_with(
|
||||||
self.my_image.resource_id,
|
self.my_image.resource_id,
|
||||||
architecture='test_architecture',
|
architecture='test_architecture',
|
||||||
kernel_id='12345678-1234-1234-1234-123456789012',
|
kernel_id='12345678-1234-1234-1234-123456789012',
|
||||||
@ -763,6 +777,17 @@ class GlanceWebImageTest(common.HeatTestCase):
|
|||||||
self.images.reactivate.assert_called_once_with(
|
self.images.reactivate.assert_called_once_with(
|
||||||
self.my_image.resource_id)
|
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):
|
def test_image_handle_update_tags(self):
|
||||||
self.my_image.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151'
|
self.my_image.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151'
|
||||||
|
|
||||||
@ -929,3 +954,67 @@ class GlanceWebImageTest(common.HeatTestCase):
|
|||||||
self.assertRaises(exception.EntityNotFound,
|
self.assertRaises(exception.EntityNotFound,
|
||||||
self.my_image.get_live_state,
|
self.my_image.get_live_state,
|
||||||
self.my_image.properties)
|
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