From 4a19755899e547d80cf453cc4b86237d5dec3810 Mon Sep 17 00:00:00 2001 From: James Slagle Date: Sun, 21 Apr 2013 18:55:23 -0400 Subject: [PATCH] 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 --- .../dashboards/admin/images/tables.py | 5 ++- .../images_and_snapshots/images/forms.py | 4 ++ .../images_and_snapshots/images/tables.py | 9 +++- .../images_and_snapshots/images/tests.py | 20 +++++++++ .../images_and_snapshots/images/views.py | 3 +- .../images/_detail_overview.html | 2 + .../project/images_and_snapshots/tests.py | 13 +++++- .../test/test_data/glance_data.py | 42 ++++++++++++++----- 8 files changed, 83 insertions(+), 15 deletions(-) diff --git a/openstack_dashboard/dashboards/admin/images/tables.py b/openstack_dashboard/dashboards/admin/images/tables.py index 1eaf61e53..aab22f238 100644 --- a/openstack_dashboard/dashboards/admin/images/tables.py +++ b/openstack_dashboard/dashboards/admin/images/tables.py @@ -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): diff --git a/openstack_dashboard/dashboards/project/images_and_snapshots/images/forms.py b/openstack_dashboard/dashboards/project/images_and_snapshots/images/forms.py index 436294166..626d182d8 100644 --- a/openstack_dashboard/dashboards/project/images_and_snapshots/images/forms.py +++ b/openstack_dashboard/dashboards/project/images_and_snapshots/images/forms.py @@ -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'], diff --git a/openstack_dashboard/dashboards/project/images_and_snapshots/images/tables.py b/openstack_dashboard/dashboards/project/images_and_snapshots/images/tables.py index b6edb6f89..0fd771a37 100644 --- a/openstack_dashboard/dashboards/project/images_and_snapshots/images/tables.py +++ b/openstack_dashboard/dashboards/project/images_and_snapshots/images/tables.py @@ -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" diff --git a/openstack_dashboard/dashboards/project/images_and_snapshots/images/tests.py b/openstack_dashboard/dashboards/project/images_and_snapshots/images/tests.py index 5f6251f29..8c3977e13 100644 --- a/openstack_dashboard/dashboards/project/images_and_snapshots/images/tests.py +++ b/openstack_dashboard/dashboards/project/images_and_snapshots/images/tests.py @@ -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): diff --git a/openstack_dashboard/dashboards/project/images_and_snapshots/images/views.py b/openstack_dashboard/dashboards/project/images_and_snapshots/images/views.py index dcc366e16..788ef302a 100644 --- a/openstack_dashboard/dashboards/project/images_and_snapshots/images/views.py +++ b/openstack_dashboard/dashboards/project/images_and_snapshots/images/views.py @@ -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): diff --git a/openstack_dashboard/dashboards/project/images_and_snapshots/templates/images_and_snapshots/images/_detail_overview.html b/openstack_dashboard/dashboards/project/images_and_snapshots/templates/images_and_snapshots/images/_detail_overview.html index 87ebafe1e..2b7d37ce2 100644 --- a/openstack_dashboard/dashboards/project/images_and_snapshots/templates/images_and_snapshots/images/_detail_overview.html +++ b/openstack_dashboard/dashboards/project/images_and_snapshots/templates/images_and_snapshots/images/_detail_overview.html @@ -14,6 +14,8 @@
{{ image.status|default:_("Unknown")|title }}
{% trans "Public" %}
{{ image.is_public }}
+
{% trans "Protected" %}
+
{{ image.protected }}
{% trans "Checksum" %}
{{ image.checksum }}
{% trans "Created" %}
diff --git a/openstack_dashboard/dashboards/project/images_and_snapshots/tests.py b/openstack_dashboard/dashboards/project/images_and_snapshots/tests.py index 0f2f29c41..fee112942 100644 --- a/openstack_dashboard/dashboards/project/images_and_snapshots/tests.py +++ b/openstack_dashboard/dashboards/project/images_and_snapshots/tests.py @@ -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')}) diff --git a/openstack_dashboard/test/test_data/glance_data.py b/openstack_dashboard/test/test_data/glance_data.py index 6dd4372be..0b5dc4c15 100644 --- a/openstack_dashboard/test/test_data/glance_data.py +++ b/openstack_dashboard/test/test_data/glance_data.py @@ -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)