From 762879a1454ab976a9b43317b1c3599c527bd834 Mon Sep 17 00:00:00 2001 From: Sampat P Date: Tue, 28 Jan 2020 11:48:28 -0500 Subject: [PATCH] [S2007220]: Added more image properties to web_image Story: 2007220 Task: 38472 Change-Id: I9980fee0b33c45e6d80862ca4a43abf075a4dd58 --- .../resources/openstack/glance/image.py | 118 +++++++++++---- heat/tests/openstack/glance/test_image.py | 134 +++++++++++++++++- ...-resource-properties-c3e06b2c98b7d127.yaml | 10 ++ 3 files changed, 235 insertions(+), 27 deletions(-) create mode 100644 releasenotes/notes/update-webimage-resource-properties-c3e06b2c98b7d127.yaml diff --git a/heat/engine/resources/openstack/glance/image.py b/heat/engine/resources/openstack/glance/image.py index 2f0053faa3..aee7601422 100644 --- a/heat/engine/resources/openstack/glance/image.py +++ b/heat/engine/resources/openstack/glance/image.py @@ -31,12 +31,12 @@ 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 + 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' + 'architecture', 'kernel_id', 'os_distro', 'os_version', + 'owner', 'visibility', 'ramdisk_id', 'active', 'members' ) glance_id_pattern = ('^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}' @@ -75,6 +75,7 @@ class GlanceWebImage(resource.Resource): properties.Schema.BOOLEAN, _('Whether the image can be deleted. If the value is True, ' 'the image is protected and cannot be deleted.'), + update_allowed=True, default=False ), DISK_FORMAT: properties.Schema( @@ -156,6 +157,28 @@ class GlanceWebImage(resource.Resource): constraints=[ constraints.AllowedPattern(glance_id_pattern) ] + ), + ACTIVE: properties.Schema( + properties.Schema.BOOLEAN, + _('Activate or deactivate the image. Requires Admin Access.'), + default=True, + update_allowed=True, + support_status=support.SupportStatus(version='16.0.0') + ), + MEMBERS: properties.Schema( + properties.Schema.LIST, + _('List of additional members that are permitted ' + 'to read the image. This may be a Keystone Project ' + 'IDs or User IDs, depending on the Glance configuration ' + 'in use.'), + schema=properties.Schema( + properties.Schema.STRING, + _('A member ID. This may be a Keystone Project ID ' + 'or User ID, depending on the Glance configuration ' + 'in use.') + ), + update_allowed=True, + support_status=support.SupportStatus(version='16.0.0') ) } @@ -166,42 +189,78 @@ class GlanceWebImage(resource.Resource): def handle_create(self): args = dict((k, v) for k, v in self.properties.items() if v is not None) - + members = args.pop(self.MEMBERS, []) + active = args.pop(self.ACTIVE) location = args.pop(self.LOCATION) images = self.client().images - image_id = images.create( - **args).id + image = images.create(**args) + image_id = image.id self.resource_id_set(image_id) - images.image_import(image_id, method='web-download', uri=location) + for member in members: + self.client().image_members.create(image_id, member) + return active - return image_id - - def check_create_complete(self, image_id): - image = self.client().images.get(image_id) - return image.status == 'active' + def check_create_complete(self, active): + image = self.client().images.get(self.resource_id) + if image.status == 'killed': + raise exception.ResourceInError( + resource_status=image.status, + ) + if not active: + if image.status == 'active': + self.client().images.deactivate(self.resource_id) + return True + elif image.status == 'deactivated': + return True + else: + return False + else: + return image.status == 'active' def handle_update(self, json_snippet, tmpl_diff, prop_diff): - if prop_diff and self.TAGS in prop_diff: - existing_tags = self.properties.get(self.TAGS) or [] - diff_tags = prop_diff.pop(self.TAGS) or [] + if prop_diff: + active = prop_diff.pop(self.ACTIVE, None) + if active is False: + self.client().images.deactivate(self.resource_id) - new_tags = set(diff_tags) - set(existing_tags) - for tag in new_tags: - self.client().image_tags.update( - self.resource_id, - tag) + if self.TAGS in prop_diff: + existing_tags = self.properties.get(self.TAGS) or [] + diff_tags = prop_diff.pop(self.TAGS) or [] - removed_tags = set(existing_tags) - set(diff_tags) - for tag in removed_tags: - with self.client_plugin().ignore_not_found: - self.client().image_tags.delete( + new_tags = set(diff_tags) - set(existing_tags) + for tag in new_tags: + self.client().image_tags.update( self.resource_id, tag) - images = self.client().images + removed_tags = set(existing_tags) - set(diff_tags) + for tag in removed_tags: + with self.client_plugin().ignore_not_found: + self.client().image_tags.delete( + self.resource_id, + tag) - 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 [] + + new_members = set(diff_members) - set(existing_members) + for _member in new_members: + self.glance().image_members.create( + self.resource_id, _member) + removed_members = set(existing_members) - set(diff_members) + for _member in removed_members: + 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): + if active: + self.client().images.reactivate(self.resource_id) + return True def validate(self): super(GlanceWebImage, self).validate() @@ -214,6 +273,13 @@ class GlanceWebImage(resource.Resource): "match.") raise exception.StackValidationFailed(message=msg) + if (self.properties[self.MEMBERS] + and self.properties[self.VISIBILITY] != 'shared'): + raise exception.ResourcePropertyValueDependency( + prop1=self.MEMBERS, + prop2=self.VISIBILITY, + value='shared') + def get_live_resource_data(self): image_data = super(GlanceWebImage, self).get_live_resource_data() if image_data.get('status') in ('deleted', 'killed'): diff --git a/heat/tests/openstack/glance/test_image.py b/heat/tests/openstack/glance/test_image.py index 62359d9e68..3f4a8c6d1a 100644 --- a/heat/tests/openstack/glance/test_image.py +++ b/heat/tests/openstack/glance/test_image.py @@ -111,6 +111,7 @@ class GlanceImageTest(common.HeatTestCase): glance.return_value = self.glanceclient self.images = self.glanceclient.images self.image_tags = self.glanceclient.image_tags + self.image_members = self.glanceclient.image_members def _test_validate(self, resource, error_msg): exc = self.assertRaises(exception.StackValidationFailed, @@ -477,6 +478,7 @@ class GlanceWebImageTest(common.HeatTestCase): glance.return_value = self.glanceclient self.images = self.glanceclient.images self.image_tags = self.glanceclient.image_tags + self.image_members = self.glanceclient.image_members def _test_validate(self, resource, error_msg): exc = self.assertRaises(exception.StackValidationFailed, @@ -640,9 +642,49 @@ class GlanceWebImageTest(common.HeatTestCase): name=u'cirros_image', protected=False, owner=u'test_owner', - tags=['tag1'], + tags=['tag1'] ) + def test_image_active_property_image_not_active(self): + self.images.reactivate.return_value = None + self.images.deactivate.return_value = None + value = mock.MagicMock() + image_id = '41f0e60c-ebb4-4375-a2b4-845ae8b9c995' + value.id = image_id + value.status = 'pending' + self.images.create.return_value = value + self.my_image.handle_create() + self.my_image.check_create_complete(image_id) + self.images.deactivate.assert_not_called() + + def test_image_active_property_image_active_to_deactivate(self): + self.images.reactivate.return_value = None + self.images.deactivate.return_value = None + value = mock.MagicMock() + image_id = '41f0e60c-ebb4-4375-a2b4-845ae8b9c995' + value.id = image_id + value.status = 'active' + self.my_image.resource_id = image_id + self.images.create.return_value = value + self.images.get.return_value = value + self.my_image.check_create_complete(False) + self.images.deactivate.assert_called_once_with( + self.my_image.resource_id) + + def test_image_active_property_image_status_killed(self): + self.images.reactivate.return_value = None + self.images.deactivate.return_value = None + value = mock.MagicMock() + image_id = '41f0e60c-ebb4-4375-a2b4-845ae8b9c995' + value.id = image_id + value.status = 'killed' + self.my_image.resource_id = image_id + self.images.create.return_value = value + self.images.get.return_value = value + ex = self.assertRaises(exception.ResourceInError, + self.my_image.check_create_complete, False) + self.assertIn('killed', ex.message) + def _handle_update_tags(self, prop_diff): self.my_image.handle_update(json_snippet=None, tmpl_diff=None, @@ -678,6 +720,49 @@ class GlanceWebImageTest(common.HeatTestCase): ramdisk_id='12345678-1234-1234-1234-123456789012' ) + def test_image_handle_update_deactivate(self): + self.images.reactivate.return_value = None + self.images.deactivate.return_value = None + value = mock.MagicMock() + image_id = '41f0e60c-ebb4-4375-a2b4-845ae8b9c995' + value.id = image_id + value.status = 'active' + self.my_image.resource_id = image_id + props = self.stack.t.t['resources']['my_image']['properties'].copy() + props['active'] = False + self.my_image.t = self.my_image.t.freeze(properties=props) + prop_diff = {'active': False} + self.my_image.reparse() + self.images.update.return_value = value + self.images.get.return_value = value + self.my_image.handle_update(json_snippet=None, + tmpl_diff=None, + prop_diff=prop_diff) + self.images.deactivate.assert_called_once_with( + self.my_image.resource_id) + + def test_image_handle_update_reactivate(self): + self.images.reactivate.return_value = None + self.images.deactivate.return_value = None + value = mock.MagicMock() + image_id = '41f0e60c-ebb4-4375-a2b4-845ae8b9c995' + value.id = image_id + value.status = 'deactivated' + self.my_image.resource_id = image_id + props = self.stack.t.t['resources']['my_image']['properties'].copy() + props['active'] = True + self.my_image.t = self.my_image.t.freeze(properties=props) + prop_diff = {'active': True} + self.my_image.reparse() + self.images.update.return_value = value + self.images.get.return_value = value + self.my_image.handle_update(json_snippet=None, + tmpl_diff=None, + prop_diff=prop_diff) + self.my_image.check_update_complete(True) + self.images.reactivate.assert_called_once_with( + self.my_image.resource_id) + def test_image_handle_update_tags(self): self.my_image.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151' @@ -707,6 +792,49 @@ class GlanceWebImageTest(common.HeatTestCase): 'tag1' ) + def _handle_update_members(self, prop_diff): + self.my_image.handle_update(json_snippet=None, + tmpl_diff=None, + prop_diff=prop_diff) + + self.image_members.create.assert_called_once_with( + self.my_image.resource_id, + 'member2' + ) + self.image_members.delete.assert_called_once_with( + self.my_image.resource_id, + 'member1' + ) + + def test_image_handle_update_members(self): + self.my_image.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151' + + props = self.stack.t.t['resources']['my_image']['properties'].copy() + props['members'] = ['member1'] + self.my_image.t = self.my_image.t.freeze(properties=props) + self.my_image.reparse() + prop_diff = {'members': ['member2']} + + self._handle_update_members(prop_diff) + + def test_image_handle_update_remove_members(self): + self.my_image.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151' + + props = self.stack.t.t['resources']['my_image']['properties'].copy() + props['members'] = ['member1'] + self.my_image.t = self.my_image.t.freeze(properties=props) + self.my_image.reparse() + prop_diff = {'members': None} + + self.my_image.handle_update(json_snippet=None, + tmpl_diff=None, + prop_diff=prop_diff) + + self.image_members.delete.assert_called_once_with( + self.my_image.resource_id, + 'member1' + ) + def test_image_handle_update_tags_delete_not_found(self): self.my_image.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151' @@ -750,6 +878,7 @@ class GlanceWebImageTest(common.HeatTestCase): 'name': 'test', 'disk_format': 'qcow2', 'container_format': 'bare', + 'active': None, 'protected': False, 'is_public': False, 'min_disk': 0, @@ -762,6 +891,7 @@ class GlanceWebImageTest(common.HeatTestCase): 'os_version': '1.0', 'owner': 'test_owner', 'ramdisk_id': '12345678-1234-1234-1234-123456789012', + 'members': None, 'visibility': 'private' } image = show_value @@ -773,6 +903,7 @@ class GlanceWebImageTest(common.HeatTestCase): 'name': 'test', 'disk_format': 'qcow2', 'container_format': 'bare', + 'active': None, 'protected': False, 'min_disk': 0, 'min_ram': 0, @@ -784,6 +915,7 @@ class GlanceWebImageTest(common.HeatTestCase): 'os_version': '1.0', 'owner': 'test_owner', 'ramdisk_id': '12345678-1234-1234-1234-123456789012', + 'members': None, 'visibility': 'private' } diff --git a/releasenotes/notes/update-webimage-resource-properties-c3e06b2c98b7d127.yaml b/releasenotes/notes/update-webimage-resource-properties-c3e06b2c98b7d127.yaml new file mode 100644 index 0000000000..6fb9a7e9d0 --- /dev/null +++ b/releasenotes/notes/update-webimage-resource-properties-c3e06b2c98b7d127.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + The ``OS::Glance::WebImage`` resource type now supports an + ``active`` property to allow administrators to deactivate + and reactivate the Image. Images remain active by default. + - | + The ``OS::Glance::WebImage`` resource type now supports a + ``members`` property for managing a list of other tenants + with access to the Image. \ No newline at end of file