summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSampat P <sp810x@att.com>2020-01-28 11:48:28 -0500
committerRico Lin <rico.lin.guanyu@gmail.com>2020-11-17 05:14:58 +0000
commit762879a1454ab976a9b43317b1c3599c527bd834 (patch)
tree1ee19e29eb3f9fc359e10650f4f91cf6687a06db
parentfa9cb978f70926dbe3380d78b3020b21fa08f7be (diff)
downloadheat-762879a1454ab976a9b43317b1c3599c527bd834.tar.gz
[S2007220]: Added more image properties to web_image
Story: 2007220 Task: 38472 Change-Id: I9980fee0b33c45e6d80862ca4a43abf075a4dd58
-rw-r--r--heat/engine/resources/openstack/glance/image.py126
-rw-r--r--heat/tests/openstack/glance/test_image.py134
-rw-r--r--releasenotes/notes/update-webimage-resource-properties-c3e06b2c98b7d127.yaml10
3 files changed, 239 insertions, 31 deletions
diff --git a/heat/engine/resources/openstack/glance/image.py b/heat/engine/resources/openstack/glance/image.py
index 2f0053faa..aee760142 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)
-
- return image_id
-
- def check_create_complete(self, image_id):
- image = self.client().images.get(image_id)
- return image.status == 'active'
+ for member in members:
+ self.client().image_members.create(image_id, member)
+ return 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 []
-
- new_tags = set(diff_tags) - set(existing_tags)
- for tag in new_tags:
- self.client().image_tags.update(
- self.resource_id,
- tag)
-
- 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(
+ if prop_diff:
+ active = prop_diff.pop(self.ACTIVE, None)
+ if active is False:
+ self.client().images.deactivate(self.resource_id)
+
+ if self.TAGS in prop_diff:
+ existing_tags = self.properties.get(self.TAGS) or []
+ diff_tags = prop_diff.pop(self.TAGS) or []
+
+ 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
-
- images.update(self.resource_id, **prop_diff)
+ 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)
+
+ 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 62359d9e6..3f4a8c6d1 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 000000000..6fb9a7e9d
--- /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