Expose "protected" attribute of images.

Adds support for displaying and setting the protected
attribute on images.  If images are protected, delete
actions are not enabled

Fixes bug 1168067

Change-Id: Iedf4fa8840c713333679762ac4fc06fd0cf1a322
This commit is contained in:
James Slagle 2013-04-21 18:55:23 -04:00
parent e4541a6175
commit 4a19755899
8 changed files with 83 additions and 15 deletions

View File

@ -29,7 +29,10 @@ class AdminCreateImage(CreateImage):
class AdminDeleteImage(DeleteImage):
def allowed(self, request, image=None):
return True
if image and image.protected:
return False
else:
return True
class AdminEditImage(EditImage):

View File

@ -86,6 +86,7 @@ class CreateImageForm(forms.SelfHandlingForm):
' minimum).'),
required=False)
is_public = forms.BooleanField(label=_("Public"), required=False)
protected = forms.BooleanField(label=_("Protected"), required=False)
def __init__(self, *args, **kwargs):
super(CreateImageForm, self).__init__(*args, **kwargs)
@ -115,6 +116,7 @@ class CreateImageForm(forms.SelfHandlingForm):
container_format = 'bare'
meta = {'is_public': data['is_public'],
'protected': data['protected'],
'disk_format': data['disk_format'],
'container_format': container_format,
'min_disk': (data['minimum_disk'] or 0),
@ -158,6 +160,7 @@ class UpdateImageForm(forms.SelfHandlingForm):
attrs={'readonly': 'readonly'}
))
public = forms.BooleanField(label=_("Public"), required=False)
protected = forms.BooleanField(label=_("Protected"), required=False)
def handle(self, request, data):
image_id = data['image_id']
@ -169,6 +172,7 @@ class UpdateImageForm(forms.SelfHandlingForm):
container_format = 'bare'
meta = {'is_public': data['public'],
'protected': data['protected'],
'disk_format': data['disk_format'],
'container_format': container_format,
'name': data['name'],

View File

@ -50,6 +50,9 @@ class DeleteImage(tables.DeleteAction):
data_type_plural = _("Images")
def allowed(self, request, image=None):
# Protected images can not be deleted.
if image and image.protected:
return False
if image:
return image.owner == request.user.tenant_id
# Return True to allow table-level bulk delete action to appear.
@ -181,6 +184,10 @@ class ImagesTable(tables.DataTable):
verbose_name=_("Public"),
empty_value=False,
filters=(filters.yesno, filters.capfirst))
protected = tables.Column("protected",
verbose_name=_("Protected"),
empty_value=False,
filters=(filters.yesno, filters.capfirst))
disk_format = tables.Column(get_format, verbose_name=_("Format"))
class Meta:
@ -190,7 +197,7 @@ class ImagesTable(tables.DataTable):
verbose_name = _("Images")
# Hide the image_type column. Done this way so subclasses still get
# all the columns by default.
columns = ["name", "status", "public", "disk_format"]
columns = ["name", "status", "public", "protected", "disk_format"]
table_actions = (OwnerFilter, CreateImage, DeleteImage,)
row_actions = (LaunchImage, EditImage, DeleteImage,)
pagination_param = "image_marker"

View File

@ -85,6 +85,7 @@ class ImageViewTests(test.TestCase):
'minimum_disk': 15,
'minimum_ram': 512,
'is_public': 1,
'protected': 0,
'method': 'CreateImageForm'}
api.glance.image_create(IsA(http.HttpRequest),
@ -92,6 +93,7 @@ class ImageViewTests(test.TestCase):
copy_from=data['copy_from'],
disk_format=data['disk_format'],
is_public=True,
protected=False,
min_disk=data['minimum_disk'],
min_ram=data['minimum_ram'],
name=data['name']). \
@ -117,12 +119,14 @@ class ImageViewTests(test.TestCase):
'minimum_disk': 15,
'minimum_ram': 512,
'is_public': 1,
'protected': 0,
'method': 'CreateImageForm'}
api.glance.image_create(IsA(http.HttpRequest),
container_format="bare",
disk_format=data['disk_format'],
is_public=True,
protected=False,
min_disk=data['minimum_disk'],
min_ram=data['minimum_ram'],
name=data['name'],
@ -150,6 +154,22 @@ class ImageViewTests(test.TestCase):
self.assertTemplateUsed(res,
'project/images_and_snapshots/images/detail.html')
self.assertEqual(res.context['image'].name, image.name)
self.assertEqual(res.context['image'].protected, image.protected)
@test.create_stubs({api.glance: ('image_get',)})
def test_protected_image_detail_get(self):
image = self.images.list()[2]
api.glance.image_get(IsA(http.HttpRequest), str(image.id)) \
.AndReturn(image)
self.mox.ReplayAll()
res = self.client.get(
reverse('horizon:project:images_and_snapshots:images:detail',
args=[image.id]))
self.assertTemplateUsed(res,
'project/images_and_snapshots/images/detail.html')
self.assertEqual(res.context['image'].protected, image.protected)
@test.create_stubs({api.glance: ('image_get',)})
def test_image_detail_get_with_exception(self):

View File

@ -76,7 +76,8 @@ class UpdateView(forms.ModalFormView):
'ramdisk': image.properties.get('ramdisk_id', ''),
'architecture': image.properties.get('architecture', ''),
'disk_format': image.disk_format,
'public': image.is_public}
'public': image.is_public,
'protected': image.protected}
class DetailView(tabs.TabView):

View File

@ -14,6 +14,8 @@
<dd>{{ image.status|default:_("Unknown")|title }}</dd>
<dt>{% trans "Public" %}</dt>
<dd>{{ image.is_public }}</dd>
<dt>{% trans "Protected" %}</dt>
<dd>{{ image.protected }}</dd>
<dt>{% trans "Checksum" %}</dt>
<dd>{{ image.checksum }}</dd>
<dt>{% trans "Created" %}</dt>

View File

@ -61,11 +61,22 @@ class ImagesAndSnapshotsTests(test.TestCase):
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'project/images_and_snapshots/index.html')
self.assertIn('images_table', res.context)
images = res.context['images_table'].data
images_table = res.context['images_table']
images = images_table.data
filter_func = lambda im: im.container_format not in ['aki', 'ari']
filtered_images = filter(filter_func, images)
self.assertItemsEqual(images, filtered_images)
self.assertTrue(len(images), 3)
row_actions = images_table.get_row_actions(images[0])
self.assertTrue(len(row_actions), 3)
row_actions = images_table.get_row_actions(images[1])
self.assertTrue(len(row_actions), 2)
self.assertTrue('delete_image' not in
[a.name for a in row_actions])
row_actions = images_table.get_row_actions(images[2])
self.assertTrue(len(row_actions), 3)
@test.create_stubs({api.glance: ('image_list_detailed',
'snapshot_list_detailed'),
api.cinder: ('volume_snapshot_list', 'volume_get')})

View File

@ -28,21 +28,24 @@ def data(TEST):
'status': "active",
'owner': TEST.tenant.id,
'properties': {'image_type': u'snapshot'},
'is_public': False}
'is_public': False,
'protected': False}
snapshot_dict_no_owner = {'name': u'snapshot 2',
'container_format': u'ami',
'id': 4,
'status': "active",
'owner': None,
'properties': {'image_type': u'snapshot'},
'is_public': False}
'is_public': False,
'protected': False}
snapshot_dict_queued = {'name': u'snapshot 2',
'container_format': u'ami',
'id': 5,
'status': "queued",
'owner': TEST.tenant.id,
'properties': {'image_type': u'snapshot'},
'is_public': False}
'is_public': False,
'protected': False}
snapshot = Image(ImageManager(None), snapshot_dict)
TEST.snapshots.add(snapshot)
snapshot = Image(ImageManager(None), snapshot_dict_no_owner)
@ -57,7 +60,8 @@ def data(TEST):
'owner': TEST.tenant.id,
'container_format': 'novaImage',
'properties': {'image_type': u'image'},
'is_public': True}
'is_public': True,
'protected': False}
public_image = Image(ImageManager(None), image_dict)
image_dict = {'id': 'a001c047-22f8-47d0-80a1-8ec94a9524fe',
@ -65,16 +69,28 @@ def data(TEST):
'status': "active",
'owner': TEST.tenant.id,
'container_format': 'aki',
'is_public': False}
'is_public': False,
'protected': False}
private_image = Image(ImageManager(None), image_dict)
image_dict = {'id': 'd6936c86-7fec-474a-85c5-5e467b371c3c',
'name': 'protected_images',
'status': "active",
'owner': TEST.tenant.id,
'container_format': 'novaImage',
'properties': {'image_type': u'image'},
'is_public': True,
'protected': True}
protected_image = Image(ImageManager(None), image_dict)
image_dict = {'id': '278905a6-4b52-4d1e-98f9-8c57bb25ba32',
'name': 'public_image 2',
'status': "active",
'owner': TEST.tenant.id,
'container_format': 'novaImage',
'properties': {'image_type': u'image'},
'is_public': True}
'is_public': True,
'protected': False}
public_image2 = Image(ImageManager(None), image_dict)
image_dict = {'id': '710a1acf-a3e3-41dd-a32d-5d6b6c86ea10',
@ -82,7 +98,8 @@ def data(TEST):
'status': "active",
'owner': TEST.tenant.id,
'container_format': 'aki',
'is_public': False}
'is_public': False,
'protected': False}
private_image2 = Image(ImageManager(None), image_dict)
image_dict = {'id': '7cd892fd-5652-40f3-a450-547615680132',
@ -90,7 +107,8 @@ def data(TEST):
'status': "active",
'owner': TEST.tenant.id,
'container_format': 'aki',
'is_public': False}
'is_public': False,
'protected': False}
private_image3 = Image(ImageManager(None), image_dict)
# A shared image. Not public and not local tenant.
@ -99,7 +117,8 @@ def data(TEST):
'status': "active",
'owner': 'someothertenant',
'container_format': 'aki',
'is_public': False}
'is_public': False,
'protected': False}
shared_image1 = Image(ImageManager(None), image_dict)
# "Official" image. Public and tenant matches an entry
@ -109,9 +128,10 @@ def data(TEST):
'status': "active",
'owner': 'officialtenant',
'container_format': 'aki',
'is_public': True}
'is_public': True,
'protected': False}
official_image1 = Image(ImageManager(None), image_dict)
TEST.images.add(public_image, private_image,
TEST.images.add(public_image, private_image, protected_image,
public_image2, private_image2, private_image3,
shared_image1, official_image1)