[S2007220]: Added more image properties to web_image
Story: 2007220 Task: 38472 Change-Id: I9980fee0b33c45e6d80862ca4a43abf075a4dd58
This commit is contained in:
parent
fa9cb978f7
commit
762879a145
@ -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'):
|
||||||
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user