[S2007220]: Added more image properties to web_image

Story: 2007220
Task: 38472

Change-Id: I9980fee0b33c45e6d80862ca4a43abf075a4dd58
This commit is contained in:
Sampat P 2020-01-28 11:48:28 -05:00 committed by Rico Lin
parent fa9cb978f7
commit 762879a145
3 changed files with 235 additions and 27 deletions

View File

@ -31,12 +31,12 @@ 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 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', 'owner', 'architecture', 'kernel_id', 'os_distro', 'os_version',
'visibility', 'ramdisk_id' 'owner', '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}'
@ -75,6 +75,7 @@ class GlanceWebImage(resource.Resource):
properties.Schema.BOOLEAN, properties.Schema.BOOLEAN,
_('Whether the image can be deleted. If the value is True, ' _('Whether the image can be deleted. If the value is True, '
'the image is protected and cannot be deleted.'), 'the image is protected and cannot be deleted.'),
update_allowed=True,
default=False default=False
), ),
DISK_FORMAT: properties.Schema( DISK_FORMAT: properties.Schema(
@ -156,6 +157,28 @@ class GlanceWebImage(resource.Resource):
constraints=[ constraints=[
constraints.AllowedPattern(glance_id_pattern) 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): 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)
members = args.pop(self.MEMBERS, [])
active = args.pop(self.ACTIVE)
location = args.pop(self.LOCATION) location = args.pop(self.LOCATION)
images = self.client().images images = self.client().images
image_id = images.create( image = images.create(**args)
**args).id image_id = image.id
self.resource_id_set(image_id) self.resource_id_set(image_id)
images.image_import(image_id, method='web-download', uri=location) 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, active):
image = self.client().images.get(self.resource_id)
def check_create_complete(self, image_id): if image.status == 'killed':
image = self.client().images.get(image_id) raise exception.ResourceInError(
return image.status == 'active' 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): def handle_update(self, json_snippet, tmpl_diff, prop_diff):
if prop_diff and self.TAGS in prop_diff: if prop_diff:
existing_tags = self.properties.get(self.TAGS) or [] active = prop_diff.pop(self.ACTIVE, None)
diff_tags = prop_diff.pop(self.TAGS) or [] if active is False:
self.client().images.deactivate(self.resource_id)
new_tags = set(diff_tags) - set(existing_tags) if self.TAGS in prop_diff:
for tag in new_tags: existing_tags = self.properties.get(self.TAGS) or []
self.client().image_tags.update( diff_tags = prop_diff.pop(self.TAGS) or []
self.resource_id,
tag)
removed_tags = set(existing_tags) - set(diff_tags) new_tags = set(diff_tags) - set(existing_tags)
for tag in removed_tags: for tag in new_tags:
with self.client_plugin().ignore_not_found: self.client().image_tags.update(
self.client().image_tags.delete(
self.resource_id, self.resource_id,
tag) 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): def validate(self):
super(GlanceWebImage, self).validate() super(GlanceWebImage, self).validate()
@ -214,6 +273,13 @@ class GlanceWebImage(resource.Resource):
"match.") "match.")
raise exception.StackValidationFailed(message=msg) 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): def get_live_resource_data(self):
image_data = super(GlanceWebImage, self).get_live_resource_data() image_data = super(GlanceWebImage, self).get_live_resource_data()
if image_data.get('status') in ('deleted', 'killed'): if image_data.get('status') in ('deleted', 'killed'):

View File

@ -111,6 +111,7 @@ class GlanceImageTest(common.HeatTestCase):
glance.return_value = self.glanceclient glance.return_value = self.glanceclient
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
def _test_validate(self, resource, error_msg): def _test_validate(self, resource, error_msg):
exc = self.assertRaises(exception.StackValidationFailed, exc = self.assertRaises(exception.StackValidationFailed,
@ -477,6 +478,7 @@ class GlanceWebImageTest(common.HeatTestCase):
glance.return_value = self.glanceclient glance.return_value = self.glanceclient
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
def _test_validate(self, resource, error_msg): def _test_validate(self, resource, error_msg):
exc = self.assertRaises(exception.StackValidationFailed, exc = self.assertRaises(exception.StackValidationFailed,
@ -640,9 +642,49 @@ class GlanceWebImageTest(common.HeatTestCase):
name=u'cirros_image', name=u'cirros_image',
protected=False, protected=False,
owner=u'test_owner', 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): 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,
@ -678,6 +720,49 @@ class GlanceWebImageTest(common.HeatTestCase):
ramdisk_id='12345678-1234-1234-1234-123456789012' 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): 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'
@ -707,6 +792,49 @@ class GlanceWebImageTest(common.HeatTestCase):
'tag1' '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): def test_image_handle_update_tags_delete_not_found(self):
self.my_image.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151' self.my_image.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151'
@ -750,6 +878,7 @@ class GlanceWebImageTest(common.HeatTestCase):
'name': 'test', 'name': 'test',
'disk_format': 'qcow2', 'disk_format': 'qcow2',
'container_format': 'bare', 'container_format': 'bare',
'active': None,
'protected': False, 'protected': False,
'is_public': False, 'is_public': False,
'min_disk': 0, 'min_disk': 0,
@ -762,6 +891,7 @@ class GlanceWebImageTest(common.HeatTestCase):
'os_version': '1.0', 'os_version': '1.0',
'owner': 'test_owner', 'owner': 'test_owner',
'ramdisk_id': '12345678-1234-1234-1234-123456789012', 'ramdisk_id': '12345678-1234-1234-1234-123456789012',
'members': None,
'visibility': 'private' 'visibility': 'private'
} }
image = show_value image = show_value
@ -773,6 +903,7 @@ class GlanceWebImageTest(common.HeatTestCase):
'name': 'test', 'name': 'test',
'disk_format': 'qcow2', 'disk_format': 'qcow2',
'container_format': 'bare', 'container_format': 'bare',
'active': None,
'protected': False, 'protected': False,
'min_disk': 0, 'min_disk': 0,
'min_ram': 0, 'min_ram': 0,
@ -784,6 +915,7 @@ class GlanceWebImageTest(common.HeatTestCase):
'os_version': '1.0', 'os_version': '1.0',
'owner': 'test_owner', 'owner': 'test_owner',
'ramdisk_id': '12345678-1234-1234-1234-123456789012', 'ramdisk_id': '12345678-1234-1234-1234-123456789012',
'members': None,
'visibility': 'private' 'visibility': 'private'
} }

View File

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