[S2007220]: Added more image properties to web_image

Change-Id: I9980fee0b33c45e6d80862ca4a43abf075a4dd58
This commit is contained in:
Sampat P 2020-01-28 11:48:28 -05:00
parent 9c6fd452b7
commit e6851f15a3
3 changed files with 219 additions and 19 deletions

View File

@ -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='15.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='15.0.0')
)
}
@ -166,20 +189,30 @@ 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 not active:
if image.status == 'active':
self.client().images.deactivate(self.resource_id)
return True
elif image.status == 'killed':
raise exception.ResourceInError(
resource_status=image.status,
)
else:
return image.status == 'active'
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
if prop_diff and self.TAGS in prop_diff:
@ -199,9 +232,27 @@ class GlanceWebImage(resource.Resource):
self.resource_id,
tag)
images = self.client().images
if self.MEMBERS in prop_diff:
existing_members = self.properties.get(self.MEMBERS) or []
diff_members = prop_diff.pop(self.MEMBERS) or []
images.update(self.resource_id, **prop_diff)
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)
active = prop_diff.pop(self.ACTIVE, None)
if active is False:
self.client().images.deactivate(self.resource_id)
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,11 +265,18 @@ 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'):
raise exception.EntityNotFound(entity='Resource',
name=self.name)
raise exception.EntityNotFound(entity='Resource',
name=self.name)
return image_data
def parse_live_resource_data(self, resource_properties, resource_data):
@ -479,8 +537,8 @@ class GlanceImage(resource.Resource):
def get_live_resource_data(self):
image_data = super(GlanceImage, self).get_live_resource_data()
if image_data.get('status') in ('deleted', 'killed'):
raise exception.EntityNotFound(entity='Resource',
name=self.name)
raise exception.EntityNotFound(entity='Resource',
name=self.name)
return image_data
def parse_live_resource_data(self, resource_properties, resource_data):

View File

@ -112,6 +112,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,
@ -478,6 +479,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,
@ -641,9 +643,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,
@ -679,6 +721,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'
@ -708,6 +793,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'
@ -751,6 +879,7 @@ class GlanceWebImageTest(common.HeatTestCase):
'name': 'test',
'disk_format': 'qcow2',
'container_format': 'bare',
'active': None,
'protected': False,
'is_public': False,
'min_disk': 0,
@ -763,6 +892,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
@ -774,6 +904,7 @@ class GlanceWebImageTest(common.HeatTestCase):
'name': 'test',
'disk_format': 'qcow2',
'container_format': 'bare',
'active': None,
'protected': False,
'min_disk': 0,
'min_ram': 0,
@ -785,6 +916,7 @@ class GlanceWebImageTest(common.HeatTestCase):
'os_version': '1.0',
'owner': 'test_owner',
'ramdisk_id': '12345678-1234-1234-1234-123456789012',
'members': None,
'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.