From 46ad19dbf08b9e313c257fa3f0ad07ec1e93ccb1 Mon Sep 17 00:00:00 2001 From: Richard Jones Date: Wed, 8 Feb 2017 16:57:47 +1100 Subject: [PATCH] Refactor Project Volumes stand-alone panel Note that there was state leaking from one of the existing tests that was relied upon in other tests and moving that test in the run order caused the others to fail. All related tests have been altered to not leak state. Change-Id: I972bc5650fa77044de8a027f570cf2cb41febef8 Implements: blueprint reorganise-volumes --- .../admin/volumes/snapshots/tables.py | 2 +- .../dashboards/admin/volumes/tabs.py | 6 +- .../dashboards/admin/volumes/tests.py | 21 +- .../dashboards/admin/volumes/volumes/forms.py | 2 +- .../admin/volumes/volumes/tables.py | 2 +- .../dashboards/admin/volumes/volumes/tests.py | 9 + .../dashboards/admin/volumes/volumes/views.py | 2 +- .../dashboards/project/backups/tables.py | 3 +- .../templates/backups/_detail_overview.html | 2 +- .../dashboards/project/backups/tests.py | 2 +- .../dashboards/project/backups/views.py | 6 +- .../project/images/images/tables.py | 2 +- .../dashboards/project/instances/views.py | 2 +- .../dashboards/project/snapshots/tables.py | 6 +- .../dashboards/project/snapshots/tests.py | 11 +- .../dashboards/project/snapshots/views.py | 2 +- .../dashboards/project/stacks/mappings.py | 4 +- .../project/volumes/{volumes => }/forms.py | 2 +- .../dashboards/project/volumes/panel.py | 1 - .../project/volumes/{volumes => }/tables.py | 26 +- .../dashboards/project/volumes/tabs.py | 116 +--- .../{volumes => }/_accept_transfer.html | 0 .../volumes/{volumes => }/_attach.html | 0 .../volumes/{volumes => }/_create.html | 2 +- .../{volumes => }/_create_snapshot.html | 2 +- .../{volumes => }/_create_transfer.html | 0 .../{volumes => }/_detail_overview.html | 2 +- .../_encryption_detail_overview.html | 0 .../volumes/{volumes => }/_extend.html | 2 +- .../volumes/{volumes => }/_extend_limits.html | 0 .../volumes/{volumes => }/_limits.html | 0 .../volumes/{volumes => }/_retype.html | 0 .../volumes/{volumes => }/_show_transfer.html | 0 .../{volumes => }/_snapshot_limits.html | 2 +- .../volumes/{volumes => }/_update.html | 0 .../{volumes => }/_upload_to_image.html | 0 .../{volumes => }/accept_transfer.html | 2 +- .../volumes/{volumes => }/attach.html | 2 +- .../volumes/{volumes => }/create.html | 2 +- .../{volumes => }/create_snapshot.html | 2 +- .../{volumes => }/create_transfer.html | 2 +- .../{volumes => }/encryption_detail.html | 2 +- .../volumes/{volumes => }/extend.html | 2 +- .../volumes/templates/volumes/index.html | 11 - .../volumes/{volumes => }/retype.html | 2 +- .../volumes/{volumes => }/show_transfer.html | 2 +- .../volumes/{volumes => }/update.html | 2 +- .../{volumes => }/upload_to_image.html | 2 +- .../dashboards/project/volumes/test.py | 175 ----- .../project/volumes/{volumes => }/tests.py | 321 ++++++--- .../dashboards/project/volumes/urls.py | 52 +- .../dashboards/project/volumes/views.py | 617 +++++++++++++++++- .../project/volumes/volumes/__init__.py | 0 .../project/volumes/volumes/tabs.py | 31 - .../project/volumes/volumes/urls.py | 64 -- .../project/volumes/volumes/views.py | 547 ---------------- ...anel.py => _1320_project_volumes_panel.py} | 2 +- 57 files changed, 988 insertions(+), 1093 deletions(-) rename openstack_dashboard/dashboards/project/volumes/{volumes => }/forms.py (99%) rename openstack_dashboard/dashboards/project/volumes/{volumes => }/tables.py (96%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/_accept_transfer.html (100%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/_attach.html (100%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/_create.html (66%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/_create_snapshot.html (83%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/_create_transfer.html (100%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/_detail_overview.html (96%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/_encryption_detail_overview.html (100%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/_extend.html (64%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/_extend_limits.html (100%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/_limits.html (100%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/_retype.html (100%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/_show_transfer.html (100%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/_snapshot_limits.html (93%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/_update.html (100%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/_upload_to_image.html (100%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/accept_transfer.html (68%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/attach.html (71%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/create.html (70%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/create_snapshot.html (68%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/create_transfer.html (68%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/encryption_detail.html (80%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/extend.html (70%) delete mode 100644 openstack_dashboard/dashboards/project/volumes/templates/volumes/index.html rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/retype.html (70%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/show_transfer.html (69%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/update.html (69%) rename openstack_dashboard/dashboards/project/volumes/templates/volumes/{volumes => }/upload_to_image.html (68%) delete mode 100644 openstack_dashboard/dashboards/project/volumes/test.py rename openstack_dashboard/dashboards/project/volumes/{volumes => }/tests.py (87%) delete mode 100644 openstack_dashboard/dashboards/project/volumes/volumes/__init__.py delete mode 100644 openstack_dashboard/dashboards/project/volumes/volumes/tabs.py delete mode 100644 openstack_dashboard/dashboards/project/volumes/volumes/urls.py delete mode 100644 openstack_dashboard/dashboards/project/volumes/volumes/views.py rename openstack_dashboard/enabled/{_1040_project_volumes_panel.py => _1320_project_volumes_panel.py} (93%) diff --git a/openstack_dashboard/dashboards/admin/volumes/snapshots/tables.py b/openstack_dashboard/dashboards/admin/volumes/snapshots/tables.py index 30855f6ceb..7979e75cf3 100644 --- a/openstack_dashboard/dashboards/admin/volumes/snapshots/tables.py +++ b/openstack_dashboard/dashboards/admin/volumes/snapshots/tables.py @@ -20,7 +20,7 @@ from openstack_dashboard.api import keystone from openstack_dashboard.dashboards.project.snapshots \ import tables as snapshots_tables -from openstack_dashboard.dashboards.project.volumes.volumes \ +from openstack_dashboard.dashboards.project.volumes \ import tables as volumes_tables diff --git a/openstack_dashboard/dashboards/admin/volumes/tabs.py b/openstack_dashboard/dashboards/admin/volumes/tabs.py index 6480741a77..ca1b57ba4c 100644 --- a/openstack_dashboard/dashboards/admin/volumes/tabs.py +++ b/openstack_dashboard/dashboards/admin/volumes/tabs.py @@ -30,11 +30,11 @@ from openstack_dashboard.dashboards.admin.volumes.volume_types \ from openstack_dashboard.dashboards.admin.volumes.volumes \ import tables as volumes_tables from openstack_dashboard.dashboards.project.volumes \ - import tabs as volumes_tabs + import views as volumes_views class VolumeTab(tables.PagedTableMixin, tabs.TableTab, - volumes_tabs.VolumeTableMixIn, tables.DataTableView): + volumes_views.VolumeTableMixIn, tables.DataTableView): table_classes = (volumes_tables.VolumesTable,) name = _("Volumes") slug = "volumes_tab" @@ -105,7 +105,7 @@ class VolumeTab(tables.PagedTableMixin, tabs.TableTab, return filters -class VolumeTypesTab(tabs.TableTab, volumes_tabs.VolumeTableMixIn): +class VolumeTypesTab(tabs.TableTab, volumes_views.VolumeTableMixIn): table_classes = (volume_types_tables.VolumeTypesTable, volume_types_tables.QosSpecsTable) name = _("Volume Types") diff --git a/openstack_dashboard/dashboards/admin/volumes/tests.py b/openstack_dashboard/dashboards/admin/volumes/tests.py index c81619507e..557c45d7e0 100644 --- a/openstack_dashboard/dashboards/admin/volumes/tests.py +++ b/openstack_dashboard/dashboards/admin/volumes/tests.py @@ -26,7 +26,7 @@ from openstack_dashboard.api import cinder from openstack_dashboard.api import keystone from openstack_dashboard.dashboards.project.snapshots \ import tables as snapshot_tables -from openstack_dashboard.dashboards.project.volumes.volumes \ +from openstack_dashboard.dashboards.project.volumes \ import tables as volume_tables from openstack_dashboard.test import helpers as test @@ -35,8 +35,16 @@ INDEX_URL = reverse('horizon:admin:volumes:index') class VolumeTests(test.BaseAdminViewTests): + def tearDown(self): + for volume in self.cinder_volumes.list(): + # VolumeTableMixIn._set_volume_attributes mutates data + # and cinder_volumes.list() doesn't deep copy + for att in volume.attachments: + if 'instance' in att: + del att['instance'] + super(VolumeTests, self).tearDown() - @test.create_stubs({api.nova: ('server_list',), + @test.create_stubs({api.nova: ('server_list', 'server_get'), cinder: ('volume_list_paged', 'volume_snapshot_list'), keystone: ('tenant_list',)}) @@ -45,6 +53,8 @@ class VolumeTests(test.BaseAdminViewTests): if instanceless_volumes: for volume in volumes: volume.attachments = [] + else: + server = self.servers.first() cinder.volume_list_paged(IsA(http.HttpRequest), sort_dir="desc", marker=None, paginate=True, @@ -53,6 +63,8 @@ class VolumeTests(test.BaseAdminViewTests): cinder.volume_snapshot_list(IsA(http.HttpRequest), search_opts={ 'all_tenants': True}).AndReturn([]) if not instanceless_volumes: + api.nova.server_get(IsA(http.HttpRequest), + server.id).AndReturn(server) api.nova.server_list(IsA(http.HttpRequest), search_opts={ 'all_tenants': True}, detailed=False) \ .AndReturn([self.servers.list(), False]) @@ -72,13 +84,14 @@ class VolumeTests(test.BaseAdminViewTests): def test_index_with_attachments(self): self._test_index(instanceless_volumes=False) - @test.create_stubs({api.nova: ('server_list',), + @test.create_stubs({api.nova: ('server_list', 'server_get'), cinder: ('volume_list_paged', 'volume_snapshot_list'), keystone: ('tenant_list',)}) def _test_index_paginated(self, marker, sort_dir, volumes, url, has_more, has_prev): vol_snaps = self.cinder_volume_snapshots.list() + server = self.servers.first() cinder.volume_list_paged(IsA(http.HttpRequest), sort_dir=sort_dir, marker=marker, paginate=True, search_opts={'all_tenants': True}) \ @@ -88,6 +101,8 @@ class VolumeTests(test.BaseAdminViewTests): api.nova.server_list(IsA(http.HttpRequest), search_opts={ 'all_tenants': True}, detailed=False) \ .AndReturn([self.servers.list(), False]) + api.nova.server_get(IsA(http.HttpRequest), + server.id).AndReturn(server) keystone.tenant_list(IsA(http.HttpRequest)) \ .AndReturn([self.tenants.list(), False]) diff --git a/openstack_dashboard/dashboards/admin/volumes/volumes/forms.py b/openstack_dashboard/dashboards/admin/volumes/volumes/forms.py index e6bc945885..b3b1962fc5 100644 --- a/openstack_dashboard/dashboards/admin/volumes/volumes/forms.py +++ b/openstack_dashboard/dashboards/admin/volumes/volumes/forms.py @@ -27,7 +27,7 @@ from horizon.utils import validators as utils_validators from openstack_dashboard.api import cinder from openstack_dashboard.dashboards.admin.volumes.snapshots.forms \ import populate_status_choices -from openstack_dashboard.dashboards.project.volumes.volumes \ +from openstack_dashboard.dashboards.project.volumes \ import forms as project_forms diff --git a/openstack_dashboard/dashboards/admin/volumes/volumes/tables.py b/openstack_dashboard/dashboards/admin/volumes/volumes/tables.py index 36db46f58c..5b5350c03c 100644 --- a/openstack_dashboard/dashboards/admin/volumes/volumes/tables.py +++ b/openstack_dashboard/dashboards/admin/volumes/volumes/tables.py @@ -16,7 +16,7 @@ from horizon import exceptions from horizon import tables from openstack_dashboard.dashboards.project.volumes \ - .volumes import tables as volumes_tables + import tables as volumes_tables class VolumesFilterAction(tables.FilterAction): diff --git a/openstack_dashboard/dashboards/admin/volumes/volumes/tests.py b/openstack_dashboard/dashboards/admin/volumes/volumes/tests.py index 642d44d896..c85ef35556 100644 --- a/openstack_dashboard/dashboards/admin/volumes/volumes/tests.py +++ b/openstack_dashboard/dashboards/admin/volumes/volumes/tests.py @@ -24,6 +24,15 @@ INDEX_URL = reverse('horizon:admin:volumes:volumes_tab') class VolumeViewTests(test.BaseAdminViewTests): + def tearDown(self): + for volume in self.cinder_volumes.list(): + # VolumeTableMixIn._set_volume_attributes mutates data + # and cinder_volumes.list() doesn't deep copy + for att in volume.attachments: + if 'instance' in att: + del att['instance'] + super(VolumeViewTests, self).tearDown() + @test.create_stubs({cinder: ('volume_reset_state', 'volume_get')}) def test_update_volume_status(self): diff --git a/openstack_dashboard/dashboards/admin/volumes/volumes/views.py b/openstack_dashboard/dashboards/admin/volumes/volumes/views.py index 2d9c64f3ba..490b938242 100644 --- a/openstack_dashboard/dashboards/admin/volumes/volumes/views.py +++ b/openstack_dashboard/dashboards/admin/volumes/volumes/views.py @@ -23,7 +23,7 @@ from openstack_dashboard.dashboards.admin.volumes.volumes \ import forms as volumes_forms from openstack_dashboard.dashboards.admin.volumes.volumes \ import tables as volumes_tables -from openstack_dashboard.dashboards.project.volumes.volumes \ +from openstack_dashboard.dashboards.project.volumes \ import views as volumes_views diff --git a/openstack_dashboard/dashboards/project/backups/tables.py b/openstack_dashboard/dashboards/project/backups/tables.py index c35eb908f1..8c7b81c67a 100644 --- a/openstack_dashboard/dashboards/project/backups/tables.py +++ b/openstack_dashboard/dashboards/project/backups/tables.py @@ -147,8 +147,7 @@ class BackupsTable(tables.DataTable): display_choices=STATUS_DISPLAY_CHOICES) volume_name = BackupVolumeNameColumn("name", verbose_name=_("Volume Name"), - link="horizon:project" - ":volumes:volumes:detail") + link="horizon:project:volumes:detail") class Meta(object): name = "volume_backups" diff --git a/openstack_dashboard/dashboards/project/backups/templates/backups/_detail_overview.html b/openstack_dashboard/dashboards/project/backups/templates/backups/_detail_overview.html index e4e950faac..b95062d7fa 100644 --- a/openstack_dashboard/dashboards/project/backups/templates/backups/_detail_overview.html +++ b/openstack_dashboard/dashboards/project/backups/templates/backups/_detail_overview.html @@ -15,7 +15,7 @@ {% if volume %}
{% trans "Volume" %}
- + {{ volume.name }}
diff --git a/openstack_dashboard/dashboards/project/backups/tests.py b/openstack_dashboard/dashboards/project/backups/tests.py index 49c7f9e341..96fa002560 100644 --- a/openstack_dashboard/dashboards/project/backups/tests.py +++ b/openstack_dashboard/dashboards/project/backups/tests.py @@ -131,7 +131,7 @@ class VolumeBackupsViewTests(test.TestCase): 'container_name': backup.container_name, 'name': backup.name, 'description': backup.description} - url = reverse('horizon:project:volumes:volumes:create_backup', + url = reverse('horizon:project:volumes:create_backup', args=[volume.id]) res = self.client.post(url, formData) diff --git a/openstack_dashboard/dashboards/project/backups/views.py b/openstack_dashboard/dashboards/project/backups/views.py index ea1a3e55e4..6cb2fbdc6b 100644 --- a/openstack_dashboard/dashboards/project/backups/views.py +++ b/openstack_dashboard/dashboards/project/backups/views.py @@ -28,11 +28,11 @@ from openstack_dashboard.dashboards.project.backups \ from openstack_dashboard.dashboards.project.backups \ import tabs as backup_tabs from openstack_dashboard.dashboards.project.volumes \ - import tabs as volume_tabs + import views as volume_views class BackupsView(tables.DataTableView, tables.PagedTableMixin, - volume_tabs.VolumeTableMixIn): + volume_views.VolumeTableMixIn): table_class = backup_tables.BackupsTable page_title = _("Volume Backups") @@ -61,7 +61,7 @@ class CreateBackupView(forms.ModalFormView): form_class = backup_forms.CreateBackupForm template_name = 'project/backups/create_backup.html' submit_label = _("Create Volume Backup") - submit_url = "horizon:project:volumes:volumes:create_backup" + submit_url = "horizon:project:volumes:create_backup" success_url = reverse_lazy("horizon:project:backups:index") page_title = _("Create Volume Backup") diff --git a/openstack_dashboard/dashboards/project/images/images/tables.py b/openstack_dashboard/dashboards/project/images/images/tables.py index e5e10b4fd7..030687d4ee 100644 --- a/openstack_dashboard/dashboards/project/images/images/tables.py +++ b/openstack_dashboard/dashboards/project/images/images/tables.py @@ -146,7 +146,7 @@ class EditImage(tables.LinkAction): class CreateVolumeFromImage(tables.LinkAction): name = "create_volume_from_image" verbose_name = _("Create Volume") - url = "horizon:project:volumes:volumes:create" + url = "horizon:project:volumes:create" classes = ("ajax-modal",) icon = "camera" policy_rules = (("volume", "volume:create"),) diff --git a/openstack_dashboard/dashboards/project/instances/views.py b/openstack_dashboard/dashboards/project/instances/views.py index 844d6075fa..2c7ecf3187 100644 --- a/openstack_dashboard/dashboards/project/instances/views.py +++ b/openstack_dashboard/dashboards/project/instances/views.py @@ -306,7 +306,7 @@ class DetailView(tabs.TabView): redirect_url = 'horizon:project:instances:index' page_title = "{{ instance.name|default:instance.id }}" image_url = 'horizon:project:images:images:detail' - volume_url = 'horizon:project:volumes:volumes:detail' + volume_url = 'horizon:project:volumes:detail' def get_context_data(self, **kwargs): context = super(DetailView, self).get_context_data(**kwargs) diff --git a/openstack_dashboard/dashboards/project/snapshots/tables.py b/openstack_dashboard/dashboards/project/snapshots/tables.py index f0c7ecfd3a..a8f0c92ad9 100644 --- a/openstack_dashboard/dashboards/project/snapshots/tables.py +++ b/openstack_dashboard/dashboards/project/snapshots/tables.py @@ -27,7 +27,7 @@ from openstack_dashboard.api import cinder from openstack_dashboard import policy from openstack_dashboard.dashboards.project.volumes \ - .volumes import tables as volume_tables + import tables as volume_tables class LaunchSnapshot(volume_tables.LaunchVolume): @@ -116,7 +116,7 @@ class EditVolumeSnapshot(policy.PolicyTargetMixin, tables.LinkAction): class CreateVolumeFromSnapshot(tables.LinkAction): name = "create_from_snapshot" verbose_name = _("Create Volume") - url = "horizon:project:volumes:volumes:create" + url = "horizon:project:volumes:create" classes = ("ajax-modal",) icon = "camera" policy_rules = (("volume", "volume:create"),) @@ -193,7 +193,7 @@ class VolumeSnapshotsTable(volume_tables.VolumesTableBase): volume_name = SnapshotVolumeNameColumn( "name", verbose_name=_("Volume Name"), - link="horizon:project:volumes:volumes:detail") + link="horizon:project:volumes:detail") class Meta(object): name = "volume_snapshots" diff --git a/openstack_dashboard/dashboards/project/snapshots/tests.py b/openstack_dashboard/dashboards/project/snapshots/tests.py index 1169b92436..2abee07e66 100644 --- a/openstack_dashboard/dashboards/project/snapshots/tests.py +++ b/openstack_dashboard/dashboards/project/snapshots/tests.py @@ -137,12 +137,11 @@ class VolumeSnapshotsViewTests(test.TestCase): AndReturn(usage_limit) self.mox.ReplayAll() - url = reverse('horizon:project:volumes:' - 'volumes:create_snapshot', args=[volume.id]) + url = reverse('horizon:project:volumes:create_snapshot', + args=[volume.id]) res = self.client.get(url) - self.assertTemplateUsed(res, 'project/volumes/volumes/' - 'create_snapshot.html') + self.assertTemplateUsed(res, 'project/volumes/create_snapshot.html') @test.create_stubs({cinder: ('volume_get', 'volume_snapshot_create',)}) @@ -165,7 +164,7 @@ class VolumeSnapshotsViewTests(test.TestCase): 'volume_id': volume.id, 'name': snapshot.name, 'description': snapshot.description} - url = reverse('horizon:project:volumes:volumes:create_snapshot', + url = reverse('horizon:project:volumes:create_snapshot', args=[volume.id]) res = self.client.post(url, formData) self.assertRedirectsNoFollow(res, INDEX_URL) @@ -191,7 +190,7 @@ class VolumeSnapshotsViewTests(test.TestCase): 'volume_id': volume.id, 'name': snapshot.name, 'description': snapshot.description} - url = reverse('horizon:project:volumes:volumes:create_snapshot', + url = reverse('horizon:project:volumes:create_snapshot', args=[volume.id]) res = self.client.post(url, formData) self.assertRedirectsNoFollow(res, INDEX_URL) diff --git a/openstack_dashboard/dashboards/project/snapshots/views.py b/openstack_dashboard/dashboards/project/snapshots/views.py index e0924dfe81..4bc701d7f4 100644 --- a/openstack_dashboard/dashboards/project/snapshots/views.py +++ b/openstack_dashboard/dashboards/project/snapshots/views.py @@ -97,7 +97,7 @@ class DetailView(tabs.TabView): tab_group_class = vol_snapshot_tabs.SnapshotDetailTabs template_name = 'horizon/common/_detail.html' page_title = "{{ snapshot.name|default:snapshot.id }}" - volume_url = 'horizon:project:volumes:volumes:detail' + volume_url = 'horizon:project:volumes:detail' def get_context_data(self, **kwargs): context = super(DetailView, self).get_context_data(**kwargs) diff --git a/openstack_dashboard/dashboards/project/stacks/mappings.py b/openstack_dashboard/dashboards/project/stacks/mappings.py index 8f03c4bf45..c1c9db16cc 100644 --- a/openstack_dashboard/dashboards/project/stacks/mappings.py +++ b/openstack_dashboard/dashboards/project/stacks/mappings.py @@ -44,13 +44,13 @@ resource_urls = { "AWS::EC2::Subnet": { 'link': 'horizon:project:networks:subnets:detail'}, "AWS::EC2::Volume": { - 'link': 'horizon:project:volumes:volumes:detail'}, + 'link': 'horizon:project:volumes:detail'}, "AWS::EC2::VPC": { 'link': 'horizon:project:networks:detail'}, "AWS::S3::Bucket": { 'link': 'horizon:project:containers:index'}, "OS::Cinder::Volume": { - 'link': 'horizon:project:volumes:volumes:detail'}, + 'link': 'horizon:project:volumes:detail'}, "OS::Heat::AccessPolicy": { 'link': 'horizon:project:stacks:detail'}, "OS::Heat::AutoScalingGroup": { diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/forms.py b/openstack_dashboard/dashboards/project/volumes/forms.py similarity index 99% rename from openstack_dashboard/dashboards/project/volumes/volumes/forms.py rename to openstack_dashboard/dashboards/project/volumes/forms.py index 2a2d588fe4..d7a8e885c5 100644 --- a/openstack_dashboard/dashboards/project/volumes/volumes/forms.py +++ b/openstack_dashboard/dashboards/project/volumes/forms.py @@ -564,7 +564,7 @@ class CreateTransferForm(forms.SelfHandlingForm): msg = _('Created volume transfer: "%s".') % data['name'] messages.success(request, msg) response = http.HttpResponseRedirect( - reverse("horizon:project:volumes:volumes:show_transfer", + reverse("horizon:project:volumes:show_transfer", args=(transfer.id, transfer.auth_key))) return response except Exception: diff --git a/openstack_dashboard/dashboards/project/volumes/panel.py b/openstack_dashboard/dashboards/project/volumes/panel.py index 88c9636096..88380610b1 100644 --- a/openstack_dashboard/dashboards/project/volumes/panel.py +++ b/openstack_dashboard/dashboards/project/volumes/panel.py @@ -18,7 +18,6 @@ import horizon class Volumes(horizon.Panel): - name = _("Volumes") slug = 'volumes' permissions = ( diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/tables.py b/openstack_dashboard/dashboards/project/volumes/tables.py similarity index 96% rename from openstack_dashboard/dashboards/project/volumes/volumes/tables.py rename to openstack_dashboard/dashboards/project/volumes/tables.py index 73164445c8..80244ad7e2 100644 --- a/openstack_dashboard/dashboards/project/volumes/volumes/tables.py +++ b/openstack_dashboard/dashboards/project/volumes/tables.py @@ -129,7 +129,7 @@ class DeleteVolume(VolumePolicyTargetMixin, tables.DeleteAction): class CreateVolume(tables.LinkAction): name = "create" verbose_name = _("Create Volume") - url = "horizon:project:volumes:volumes:create" + url = "horizon:project:volumes:create" classes = ("ajax-modal", "btn-create") icon = "plus" policy_rules = (("volume", "volume:create"),) @@ -166,7 +166,7 @@ class CreateVolume(tables.LinkAction): class ExtendVolume(VolumePolicyTargetMixin, tables.LinkAction): name = "extend" verbose_name = _("Extend Volume") - url = "horizon:project:volumes:volumes:extend" + url = "horizon:project:volumes:extend" classes = ("ajax-modal", "btn-extend") policy_rules = (("volume", "volume:extend"),) @@ -177,7 +177,7 @@ class ExtendVolume(VolumePolicyTargetMixin, tables.LinkAction): class EditAttachments(tables.LinkAction): name = "attachments" verbose_name = _("Manage Attachments") - url = "horizon:project:volumes:volumes:attach" + url = "horizon:project:volumes:attach" classes = ("ajax-modal",) icon = "pencil" @@ -204,7 +204,7 @@ class EditAttachments(tables.LinkAction): class CreateSnapshot(VolumePolicyTargetMixin, tables.LinkAction): name = "snapshots" verbose_name = _("Create Snapshot") - url = "horizon:project:volumes:volumes:create_snapshot" + url = "horizon:project:volumes:create_snapshot" classes = ("ajax-modal",) icon = "camera" policy_rules = (("volume", "volume:create_snapshot"),) @@ -229,7 +229,7 @@ class CreateSnapshot(VolumePolicyTargetMixin, tables.LinkAction): class CreateTransfer(VolumePolicyTargetMixin, tables.LinkAction): name = "create_transfer" verbose_name = _("Create Transfer") - url = "horizon:project:volumes:volumes:create_transfer" + url = "horizon:project:volumes:create_transfer" classes = ("ajax-modal",) policy_rules = (("volume", "volume:create_transfer"),) @@ -240,7 +240,7 @@ class CreateTransfer(VolumePolicyTargetMixin, tables.LinkAction): class CreateBackup(VolumePolicyTargetMixin, tables.LinkAction): name = "backups" verbose_name = _("Create Backup") - url = "horizon:project:volumes:volumes:create_backup" + url = "horizon:project:volumes:create_backup" classes = ("ajax-modal",) policy_rules = (("volume", "backup:create"),) @@ -252,7 +252,7 @@ class CreateBackup(VolumePolicyTargetMixin, tables.LinkAction): class UploadToImage(VolumePolicyTargetMixin, tables.LinkAction): name = "upload_to_image" verbose_name = _("Upload to Image") - url = "horizon:project:volumes:volumes:upload_to_image" + url = "horizon:project:volumes:upload_to_image" classes = ("ajax-modal",) icon = "cloud-upload" policy_rules = (("volume", @@ -269,7 +269,7 @@ class UploadToImage(VolumePolicyTargetMixin, tables.LinkAction): class EditVolume(VolumePolicyTargetMixin, tables.LinkAction): name = "edit" verbose_name = _("Edit Volume") - url = "horizon:project:volumes:volumes:update" + url = "horizon:project:volumes:update" classes = ("ajax-modal",) icon = "pencil" policy_rules = (("volume", "volume:update"),) @@ -281,7 +281,7 @@ class EditVolume(VolumePolicyTargetMixin, tables.LinkAction): class RetypeVolume(VolumePolicyTargetMixin, tables.LinkAction): name = "retype" verbose_name = _("Change Volume Type") - url = "horizon:project:volumes:volumes:retype" + url = "horizon:project:volumes:retype" classes = ("ajax-modal",) icon = "pencil" policy_rules = (("volume", "volume:retype"),) @@ -293,7 +293,7 @@ class RetypeVolume(VolumePolicyTargetMixin, tables.LinkAction): class AcceptTransfer(tables.LinkAction): name = "accept_transfer" verbose_name = _("Accept Transfer") - url = "horizon:project:volumes:volumes:accept_transfer" + url = "horizon:project:volumes:accept_transfer" classes = ("ajax-modal",) icon = "exchange" policy_rules = (("volume", "volume:accept_transfer"),) @@ -401,7 +401,7 @@ def get_encrypted_value(volume): def get_encrypted_link(volume): if hasattr(volume, 'encrypted') and volume.encrypted: - return reverse("horizon:project:volumes:volumes:encryption_detail", + return reverse("horizon:project:volumes:encryption_detail", kwargs={'volume_id': volume.id}) @@ -444,7 +444,7 @@ class VolumesTableBase(tables.DataTable): ) name = tables.Column("name", verbose_name=_("Name"), - link="horizon:project:volumes:volumes:detail") + link="horizon:project:volumes:detail") description = tables.Column("description", verbose_name=_("Description"), truncate=40) @@ -490,7 +490,7 @@ class UpdateMetadata(tables.LinkAction): class VolumesTable(VolumesTableBase): name = tables.WrappingColumn("name", verbose_name=_("Name"), - link="horizon:project:volumes:volumes:detail") + link="horizon:project:volumes:detail") volume_type = tables.Column(get_volume_type, verbose_name=_("Type")) attachments = AttachmentColumn("attachments", diff --git a/openstack_dashboard/dashboards/project/volumes/tabs.py b/openstack_dashboard/dashboards/project/volumes/tabs.py index 4196f23991..5f48ea88b2 100644 --- a/openstack_dashboard/dashboards/project/volumes/tabs.py +++ b/openstack_dashboard/dashboards/project/volumes/tabs.py @@ -1,4 +1,4 @@ -# Copyright 2013 Nebula, Inc. +# Copyright 2012 Nebula, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -12,114 +12,20 @@ # License for the specific language governing permissions and limitations # under the License. -from collections import OrderedDict - from django.utils.translation import ugettext_lazy as _ -from horizon import exceptions -from horizon.tables import PagedTableMixin from horizon import tabs -from openstack_dashboard import api -from openstack_dashboard.dashboards.project.volumes.volumes \ - import tables as volume_tables +class OverviewTab(tabs.Tab): + name = _("Overview") + slug = "overview" + template_name = ("project/volumes/_detail_overview.html") + + def get_context_data(self, request): + return {"volume": self.tab_group.kwargs['volume']} -class VolumeTableMixIn(object): - _has_more_data = False - _has_prev_data = False - - def _get_volumes(self, search_opts=None): - try: - marker, sort_dir = self._get_marker() - volumes, self._has_more_data, self._has_prev_data = \ - api.cinder.volume_list_paged(self.request, marker=marker, - search_opts=search_opts, - sort_dir=sort_dir, paginate=True) - - if sort_dir == "asc": - volumes.reverse() - - return volumes - except Exception: - exceptions.handle(self.request, - _('Unable to retrieve volume list.')) - return [] - - def _get_instances(self, search_opts=None, instance_ids=None): - if not instance_ids: - return [] - try: - # TODO(tsufiev): we should pass attached_instance_ids to - # nova.server_list as soon as Nova API allows for this - instances, has_more = api.nova.server_list(self.request, - search_opts=search_opts, - detailed=False) - return instances - except Exception: - exceptions.handle(self.request, - _("Unable to retrieve volume/instance " - "attachment information")) - return [] - - def _get_volumes_ids_with_snapshots(self, search_opts=None): - try: - volume_ids = [] - snapshots = api.cinder.volume_snapshot_list( - self.request, search_opts=search_opts) - if snapshots: - # extract out the volume ids - volume_ids = set([(s.volume_id) for s in snapshots]) - except Exception: - exceptions.handle(self.request, - _("Unable to retrieve snapshot list.")) - - return volume_ids - - def _get_attached_instance_ids(self, volumes): - attached_instance_ids = [] - for volume in volumes: - for att in volume.attachments: - server_id = att.get('server_id', None) - if server_id is not None: - attached_instance_ids.append(server_id) - return attached_instance_ids - - # set attachment string and if volume has snapshots - def _set_volume_attributes(self, - volumes, - instances, - volume_ids_with_snapshots): - instances = OrderedDict([(inst.id, inst) for inst in instances]) - for volume in volumes: - if volume_ids_with_snapshots: - if volume.id in volume_ids_with_snapshots: - setattr(volume, 'has_snapshot', True) - if instances: - for att in volume.attachments: - server_id = att.get('server_id', None) - att['instance'] = instances.get(server_id, None) - - -class VolumeTab(PagedTableMixin, tabs.TableTab, VolumeTableMixIn): - table_classes = (volume_tables.VolumesTable,) - name = _("Volumes") - slug = "volumes_tab" - template_name = ("horizon/common/_detail_table.html") - preload = False - - def get_volumes_data(self): - volumes = self._get_volumes() - attached_instance_ids = self._get_attached_instance_ids(volumes) - instances = self._get_instances(instance_ids=attached_instance_ids) - volume_ids_with_snapshots = self._get_volumes_ids_with_snapshots() - self._set_volume_attributes( - volumes, instances, volume_ids_with_snapshots) - return volumes - - -class VolumeAndSnapshotTabs(tabs.TabGroup): - slug = "volumes_and_snapshots" - tabs = (VolumeTab, ) - sticky = True +class VolumeDetailTabs(tabs.TabGroup): + slug = "volume_details" + tabs = (OverviewTab,) diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_accept_transfer.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/_accept_transfer.html similarity index 100% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_accept_transfer.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/_accept_transfer.html diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_attach.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/_attach.html similarity index 100% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_attach.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/_attach.html diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_create.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/_create.html similarity index 66% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_create.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/_create.html index 3d08c83b5f..d7cad6ddb7 100644 --- a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_create.html +++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/_create.html @@ -3,6 +3,6 @@ {% block modal-body-right %}
- {% include "project/volumes/volumes/_limits.html" with usages=usages %} + {% include "project/volumes/_limits.html" with usages=usages %}
{% endblock %} diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_create_snapshot.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/_create_snapshot.html similarity index 83% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_create_snapshot.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/_create_snapshot.html index dc689c8d2c..abb88b9c5f 100644 --- a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_create_snapshot.html +++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/_create_snapshot.html @@ -3,7 +3,7 @@ {% block modal-body-right %}
- {% include "project/volumes/volumes/_snapshot_limits.html" with usages=usages snapshot_quota=True %} + {% include "project/volumes/_snapshot_limits.html" with usages=usages snapshot_quota=True %}
{% endblock %} diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_create_transfer.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/_create_transfer.html similarity index 100% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_create_transfer.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/_create_transfer.html diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_detail_overview.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/_detail_overview.html similarity index 96% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_detail_overview.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/_detail_overview.html index b057690b08..2358b229ea 100644 --- a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_detail_overview.html +++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/_detail_overview.html @@ -31,7 +31,7 @@
{{ volume.is_bootable|yesno|capfirst }}
{% trans "Encrypted" %}
{% if volume.encrypted %} -
{% trans "Yes" %}
+
{% trans "Yes" %}
{% else %}
{% trans "No" %}
{% endif %} diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_encryption_detail_overview.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/_encryption_detail_overview.html similarity index 100% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_encryption_detail_overview.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/_encryption_detail_overview.html diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_extend.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/_extend.html similarity index 64% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_extend.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/_extend.html index 6a1eeb599f..b41a9f1d17 100644 --- a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_extend.html +++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/_extend.html @@ -3,6 +3,6 @@ {% block modal-body-right %}
- {% include "project/volumes/volumes/_extend_limits.html" with usages=usages %} + {% include "project/volumes/_extend_limits.html" with usages=usages %}
{% endblock %} diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_extend_limits.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/_extend_limits.html similarity index 100% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_extend_limits.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/_extend_limits.html diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_limits.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/_limits.html similarity index 100% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_limits.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/_limits.html diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_retype.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/_retype.html similarity index 100% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_retype.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/_retype.html diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_show_transfer.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/_show_transfer.html similarity index 100% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_show_transfer.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/_show_transfer.html diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_snapshot_limits.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/_snapshot_limits.html similarity index 93% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_snapshot_limits.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/_snapshot_limits.html index 1f3c5323f2..e7cbc48500 100644 --- a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_snapshot_limits.html +++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/_snapshot_limits.html @@ -1,4 +1,4 @@ -{% extends "project/volumes/volumes/_limits.html" %} +{% extends "project/volumes/_limits.html" %} {% load i18n horizon humanize %} {% block title %} diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_update.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/_update.html similarity index 100% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_update.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/_update.html diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_upload_to_image.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/_upload_to_image.html similarity index 100% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_upload_to_image.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/_upload_to_image.html diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/accept_transfer.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/accept_transfer.html similarity index 68% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/accept_transfer.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/accept_transfer.html index 079f9a1749..1106b99bbf 100644 --- a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/accept_transfer.html +++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/accept_transfer.html @@ -3,5 +3,5 @@ {% block title %}{% trans "Accept Volume Transfer" %}{% endblock %} {% block main %} - {% include 'project/volumes/volumes/_accept_transfer.html' %} + {% include 'project/volumes/_accept_transfer.html' %} {% endblock %} diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/attach.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/attach.html similarity index 71% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/attach.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/attach.html index 96d75a75f1..29f263dcc3 100644 --- a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/attach.html +++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/attach.html @@ -3,5 +3,5 @@ {% block title %}{% trans "Manage Volume Attachments" %}{% endblock %} {% block main %} - {% include 'project/volumes/volumes/_attach.html' %} + {% include 'project/volumes/_attach.html' %} {% endblock %} diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/create.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/create.html similarity index 70% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/create.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/create.html index 8d3209f4eb..1c7973b4f4 100644 --- a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/create.html +++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/create.html @@ -3,5 +3,5 @@ {% block title %}{% trans "Create Volume" %}{% endblock %} {% block main %} - {% include 'project/volumes/volumes/_create.html' %} + {% include 'project/volumes/_create.html' %} {% endblock %} diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/create_snapshot.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/create_snapshot.html similarity index 68% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/create_snapshot.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/create_snapshot.html index 55413883cf..13fe8e312d 100644 --- a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/create_snapshot.html +++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/create_snapshot.html @@ -3,5 +3,5 @@ {% block title %}{% trans "Create Volume Snapshot" %}{% endblock %} {% block main %} - {% include 'project/volumes/volumes/_create_snapshot.html' %} + {% include 'project/volumes/_create_snapshot.html' %} {% endblock %} diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/create_transfer.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/create_transfer.html similarity index 68% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/create_transfer.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/create_transfer.html index 6b35292f30..761448f7f0 100644 --- a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/create_transfer.html +++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/create_transfer.html @@ -3,5 +3,5 @@ {% block title %}{% trans "Create Volume Transfer" %}{% endblock %} {% block main %} - {% include 'project/volumes/volumes/_create_transfer.html' %} + {% include 'project/volumes/_create_transfer.html' %} {% endblock %} diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/encryption_detail.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/encryption_detail.html similarity index 80% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/encryption_detail.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/encryption_detail.html index 5881a4afcd..e01894ebe6 100644 --- a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/encryption_detail.html +++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/encryption_detail.html @@ -9,7 +9,7 @@ {% block main %}
- {% include "project/volumes/volumes/_encryption_detail_overview.html" %} + {% include "project/volumes/_encryption_detail_overview.html" %}
{% endblock %} diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/extend.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/extend.html similarity index 70% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/extend.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/extend.html index 00b29e8441..d5fe67881e 100644 --- a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/extend.html +++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/extend.html @@ -3,5 +3,5 @@ {% block title %}{% trans "Extend Volume" %}{% endblock %} {% block main %} - {% include 'project/volumes/volumes/_extend.html' %} + {% include 'project/volumes/_extend.html' %} {% endblock %} diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/index.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/index.html deleted file mode 100644 index 4b518c863d..0000000000 --- a/openstack_dashboard/dashboards/project/volumes/templates/volumes/index.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} -{% block title %}{% trans "Volumes" %}{% endblock %} - -{% block main %} -
-
- {{ tab_group.render }} -
-
-{% endblock %} diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/retype.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/retype.html similarity index 70% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/retype.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/retype.html index 3316d9af02..ea58f53ab6 100644 --- a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/retype.html +++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/retype.html @@ -3,5 +3,5 @@ {% block title %}{% trans "Change Volume Type" %}{% endblock %} {% block main %} - {% include 'project/volumes/volumes/_retype.html' %} + {% include 'project/volumes/_retype.html' %} {% endblock %} diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/show_transfer.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/show_transfer.html similarity index 69% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/show_transfer.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/show_transfer.html index debf30847a..d0e701cd41 100644 --- a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/show_transfer.html +++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/show_transfer.html @@ -3,5 +3,5 @@ {% block title %}{% trans "Volume Transfer Details" %}{% endblock %} {% block main %} - {% include 'project/volumes/volumes/_show_transfer.html' %} + {% include 'project/volumes/_show_transfer.html' %} {% endblock %} diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/update.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/update.html similarity index 69% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/update.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/update.html index 85c573b486..9bae6c483c 100644 --- a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/update.html +++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/update.html @@ -3,5 +3,5 @@ {% block title %}{% trans "Edit Volume" %}{% endblock %} {% block main %} - {% include 'project/volumes/volumes/_update.html' %} + {% include 'project/volumes/_update.html' %} {% endblock %} diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/upload_to_image.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/upload_to_image.html similarity index 68% rename from openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/upload_to_image.html rename to openstack_dashboard/dashboards/project/volumes/templates/volumes/upload_to_image.html index 519eec32c7..a405ea1f69 100644 --- a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/upload_to_image.html +++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/upload_to_image.html @@ -3,5 +3,5 @@ {% block title %}{% trans "Upload Volume to Image" %}{% endblock %} {% block main %} - {% include 'project/volumes/volumes/_upload_to_image.html' %} + {% include 'project/volumes/_upload_to_image.html' %} {% endblock %} diff --git a/openstack_dashboard/dashboards/project/volumes/test.py b/openstack_dashboard/dashboards/project/volumes/test.py deleted file mode 100644 index 9b5f00c837..0000000000 --- a/openstack_dashboard/dashboards/project/volumes/test.py +++ /dev/null @@ -1,175 +0,0 @@ -# Copyright 2012 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import copy - -from django.conf import settings -from django.core.urlresolvers import reverse -from django import http -from django.test.utils import override_settings -from django.utils.http import urlunquote - -from mox3.mox import IsA # noqa - -from openstack_dashboard import api -from openstack_dashboard.dashboards.project.volumes.volumes \ - import tables as volume_tables -from openstack_dashboard.test import helpers as test - - -INDEX_URL = reverse('horizon:project:volumes:index') - - -class VolumeAndSnapshotsAndBackupsTests(test.TestCase): - @test.create_stubs({api.cinder: ('tenant_absolute_limits', - 'volume_list', - 'volume_list_paged', - 'volume_snapshot_list', - 'volume_backup_supported', - 'volume_backup_list_paged', - ), - api.nova: ('server_list',)}) - def test_index(self, instanceless_volumes=False): - vol_snaps = self.cinder_volume_snapshots.list() - volumes = self.cinder_volumes.list() - if instanceless_volumes: - for volume in volumes: - volume.attachments = [] - - api.cinder.volume_backup_supported(IsA(http.HttpRequest)).\ - MultipleTimes().AndReturn(False) - api.cinder.volume_list_paged( - IsA(http.HttpRequest), marker=None, search_opts=None, - sort_dir='desc', paginate=True).\ - AndReturn([volumes, False, False]) - if not instanceless_volumes: - api.nova.server_list(IsA(http.HttpRequest), search_opts=None, - detailed=False).\ - AndReturn([self.servers.list(), False]) - api.cinder.volume_snapshot_list(IsA(http.HttpRequest)).\ - AndReturn(vol_snaps) - - api.cinder.tenant_absolute_limits(IsA(http.HttpRequest)).\ - MultipleTimes().AndReturn(self.cinder_limits['absolute']) - self.mox.ReplayAll() - - res = self.client.get(INDEX_URL) - self.assertEqual(res.status_code, 200) - self.assertTemplateUsed(res, 'project/volumes/index.html') - - def test_index_no_volume_attachments(self): - self.test_index(instanceless_volumes=True) - - @test.create_stubs({api.cinder: ('tenant_absolute_limits', - 'volume_list_paged', - 'volume_backup_supported', - 'volume_snapshot_list'), - api.nova: ('server_list',)}) - def _test_index_paginated(self, marker, sort_dir, volumes, url, - has_more, has_prev): - backup_supported = True - vol_snaps = self.cinder_volume_snapshots.list() - - api.cinder.volume_backup_supported(IsA(http.HttpRequest)).\ - MultipleTimes().AndReturn(backup_supported) - api.cinder.volume_list_paged(IsA(http.HttpRequest), marker=marker, - sort_dir=sort_dir, search_opts=None, - paginate=True).\ - AndReturn([volumes, has_more, has_prev]) - api.cinder.volume_snapshot_list( - IsA(http.HttpRequest), search_opts=None).AndReturn(vol_snaps) - api.nova.server_list(IsA(http.HttpRequest), search_opts=None, - detailed=False).\ - AndReturn([self.servers.list(), False]) - api.cinder.tenant_absolute_limits(IsA(http.HttpRequest)).MultipleTimes().\ - AndReturn(self.cinder_limits['absolute']) - self.mox.ReplayAll() - - res = self.client.get(urlunquote(url)) - self.assertEqual(res.status_code, 200) - self.assertTemplateUsed(res, 'project/volumes/index.html') - - self.mox.UnsetStubs() - return res - - def ensure_attachments_exist(self, volumes): - volumes = copy.copy(volumes) - for volume in volumes: - if not volume.attachments: - volume.attachments.append({ - "id": "1", "server_id": '1', "device": "/dev/hda"}) - return volumes - - @override_settings(API_RESULT_PAGE_SIZE=2) - def test_index_paginated(self): - mox_volumes = self.ensure_attachments_exist(self.cinder_volumes.list()) - size = settings.API_RESULT_PAGE_SIZE - - # get first page - expected_volumes = mox_volumes[:size] - url = INDEX_URL - res = self._test_index_paginated(marker=None, sort_dir="desc", - volumes=expected_volumes, url=url, - has_more=True, has_prev=False) - volumes = res.context['volumes_table'].data - self.assertItemsEqual(volumes, expected_volumes) - - # get second page - expected_volumes = mox_volumes[size:2 * size] - marker = expected_volumes[0].id - next = volume_tables.VolumesTable._meta.pagination_param - url = "?".join([INDEX_URL, "=".join([next, marker])]) - res = self._test_index_paginated(marker=marker, sort_dir="desc", - volumes=expected_volumes, url=url, - has_more=True, has_prev=True) - volumes = res.context['volumes_table'].data - self.assertItemsEqual(volumes, expected_volumes) - - # get last page - expected_volumes = mox_volumes[-size:] - marker = expected_volumes[0].id - next = volume_tables.VolumesTable._meta.pagination_param - url = "?".join([INDEX_URL, "=".join([next, marker])]) - res = self._test_index_paginated(marker=marker, sort_dir="desc", - volumes=expected_volumes, url=url, - has_more=False, has_prev=True) - volumes = res.context['volumes_table'].data - self.assertItemsEqual(volumes, expected_volumes) - - @override_settings(API_RESULT_PAGE_SIZE=2) - def test_index_paginated_prev_page(self): - mox_volumes = self.ensure_attachments_exist(self.cinder_volumes.list()) - size = settings.API_RESULT_PAGE_SIZE - - # prev from some page - expected_volumes = mox_volumes[size:2 * size] - marker = expected_volumes[0].id - prev = volume_tables.VolumesTable._meta.prev_pagination_param - url = "?".join([INDEX_URL, "=".join([prev, marker])]) - res = self._test_index_paginated(marker=marker, sort_dir="asc", - volumes=expected_volumes, url=url, - has_more=True, has_prev=True) - volumes = res.context['volumes_table'].data - self.assertItemsEqual(volumes, expected_volumes) - - # back to first page - expected_volumes = mox_volumes[:size] - marker = expected_volumes[0].id - prev = volume_tables.VolumesTable._meta.prev_pagination_param - url = "?".join([INDEX_URL, "=".join([prev, marker])]) - res = self._test_index_paginated(marker=marker, sort_dir="asc", - volumes=expected_volumes, url=url, - has_more=True, has_prev=False) - volumes = res.context['volumes_table'].data - self.assertItemsEqual(volumes, expected_volumes) diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/tests.py b/openstack_dashboard/dashboards/project/volumes/tests.py similarity index 87% rename from openstack_dashboard/dashboards/project/volumes/volumes/tests.py rename to openstack_dashboard/dashboards/project/volumes/tests.py index 253a3153e6..c4db43f5c1 100644 --- a/openstack_dashboard/dashboards/project/volumes/volumes/tests.py +++ b/openstack_dashboard/dashboards/project/volumes/tests.py @@ -1,7 +1,3 @@ -# Copyright 2012 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# # Copyright 2012 Nebula, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -16,7 +12,12 @@ # License for the specific language governing permissions and limitations # under the License. +import copy +import six +from six import moves + import django +from django.conf import settings from django.core.urlresolvers import reverse from django.forms import widgets from django import http @@ -25,22 +26,179 @@ from django.test.utils import override_settings from django.utils.http import urlunquote from mox3.mox import IsA # noqa -import six -from six import moves from openstack_dashboard import api from openstack_dashboard.api import cinder +from openstack_dashboard.dashboards.project.volumes \ + import tables as volume_tables from openstack_dashboard.test import helpers as test from openstack_dashboard.usage import quotas -VOLUME_INDEX_URL = reverse('horizon:project:volumes:index') -VOLUME_VOLUMES_TAB_URL = urlunquote(reverse( - 'horizon:project:volumes:volumes_tab')) +INDEX_URL = reverse('horizon:project:volumes:index') SEARCH_OPTS = dict(status=api.cinder.VOLUME_STATE_AVAILABLE) class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): + def tearDown(self): + for volume in self.cinder_volumes.list(): + # VolumeTableMixIn._set_volume_attributes mutates data + # and cinder_volumes.list() doesn't deep copy + for att in volume.attachments: + if 'instance' in att: + del att['instance'] + super(VolumeViewTests, self).tearDown() + + @test.create_stubs({api.cinder: ('tenant_absolute_limits', + 'volume_list', + 'volume_list_paged', + 'volume_snapshot_list', + 'volume_backup_supported', + 'volume_backup_list_paged', + ), + api.nova: ('server_list', 'server_get')}) + def test_index(self, with_attachments=True): + vol_snaps = self.cinder_volume_snapshots.list() + volumes = self.cinder_volumes.list() + if with_attachments: + server = self.servers.first() + else: + for volume in volumes: + volume.attachments = [] + + api.cinder.volume_backup_supported(IsA(http.HttpRequest)).\ + MultipleTimes().AndReturn(False) + api.cinder.volume_list_paged( + IsA(http.HttpRequest), marker=None, search_opts=None, + sort_dir='desc', paginate=True).\ + AndReturn([volumes, False, False]) + if with_attachments: + api.nova.server_get(IsA(http.HttpRequest), + server.id).AndReturn(server) + + api.nova.server_list(IsA(http.HttpRequest), search_opts=None, + detailed=False).\ + AndReturn([self.servers.list(), False]) + api.cinder.volume_snapshot_list(IsA(http.HttpRequest)). \ + AndReturn(vol_snaps) + + api.cinder.tenant_absolute_limits(IsA(http.HttpRequest)).\ + MultipleTimes().AndReturn(self.cinder_limits['absolute']) + self.mox.ReplayAll() + + res = self.client.get(INDEX_URL) + self.assertEqual(res.status_code, 200) + self.assertTemplateUsed(res, 'horizon/common/_data_table_view.html') + + def test_index_no_volume_attachments(self): + self.test_index(with_attachments=False) + + @test.create_stubs({api.cinder: ('tenant_absolute_limits', + 'volume_list_paged', + 'volume_backup_supported', + 'volume_snapshot_list'), + api.nova: ('server_list', 'server_get')}) + def _test_index_paginated(self, marker, sort_dir, volumes, url, + has_more, has_prev): + backup_supported = True + vol_snaps = self.cinder_volume_snapshots.list() + server = self.servers.first() + + api.cinder.volume_backup_supported(IsA(http.HttpRequest)).\ + MultipleTimes().AndReturn(backup_supported) + api.cinder.volume_list_paged(IsA(http.HttpRequest), marker=marker, + sort_dir=sort_dir, search_opts=None, + paginate=True).\ + AndReturn([volumes, has_more, has_prev]) + api.cinder.volume_snapshot_list( + IsA(http.HttpRequest), search_opts=None).AndReturn(vol_snaps) + api.nova.server_list(IsA(http.HttpRequest), search_opts=None, + detailed=False).\ + AndReturn([self.servers.list(), False]) + api.nova.server_get(IsA(http.HttpRequest), + server.id).AndReturn(server) + api.cinder.tenant_absolute_limits(IsA(http.HttpRequest)).MultipleTimes().\ + AndReturn(self.cinder_limits['absolute']) + self.mox.ReplayAll() + + res = self.client.get(urlunquote(url)) + self.assertEqual(res.status_code, 200) + self.assertTemplateUsed(res, 'horizon/common/_data_table_view.html') + + self.mox.UnsetStubs() + return res + + def ensure_attachments_exist(self, volumes): + volumes = copy.copy(volumes) + for volume in volumes: + if not volume.attachments: + volume.attachments.append({ + "id": "1", "server_id": '1', "device": "/dev/hda"}) + return volumes + + @override_settings(API_RESULT_PAGE_SIZE=2) + def test_index_paginated(self): + mox_volumes = self.ensure_attachments_exist(self.cinder_volumes.list()) + size = settings.API_RESULT_PAGE_SIZE + + # get first page + expected_volumes = mox_volumes[:size] + url = INDEX_URL + res = self._test_index_paginated(marker=None, sort_dir="desc", + volumes=expected_volumes, url=url, + has_more=True, has_prev=False) + volumes = res.context['volumes_table'].data + self.assertItemsEqual(volumes, expected_volumes) + + # get second page + expected_volumes = mox_volumes[size:2 * size] + marker = expected_volumes[0].id + next = volume_tables.VolumesTable._meta.pagination_param + url = "?".join([INDEX_URL, "=".join([next, marker])]) + res = self._test_index_paginated(marker=marker, sort_dir="desc", + volumes=expected_volumes, url=url, + has_more=True, has_prev=True) + volumes = res.context['volumes_table'].data + self.assertItemsEqual(volumes, expected_volumes) + + # get last page + expected_volumes = mox_volumes[-size:] + marker = expected_volumes[0].id + next = volume_tables.VolumesTable._meta.pagination_param + url = "?".join([INDEX_URL, "=".join([next, marker])]) + res = self._test_index_paginated(marker=marker, sort_dir="desc", + volumes=expected_volumes, url=url, + has_more=False, has_prev=True) + volumes = res.context['volumes_table'].data + self.assertItemsEqual(volumes, expected_volumes) + + @override_settings(API_RESULT_PAGE_SIZE=2) + def test_index_paginated_prev_page(self): + mox_volumes = self.ensure_attachments_exist(self.cinder_volumes.list()) + size = settings.API_RESULT_PAGE_SIZE + + # prev from some page + expected_volumes = mox_volumes[size:2 * size] + marker = expected_volumes[0].id + prev = volume_tables.VolumesTable._meta.prev_pagination_param + url = "?".join([INDEX_URL, "=".join([prev, marker])]) + res = self._test_index_paginated(marker=marker, sort_dir="asc", + volumes=expected_volumes, url=url, + has_more=True, has_prev=True) + volumes = res.context['volumes_table'].data + self.assertItemsEqual(volumes, expected_volumes) + + # back to first page + expected_volumes = mox_volumes[:size] + marker = expected_volumes[0].id + prev = volume_tables.VolumesTable._meta.prev_pagination_param + url = "?".join([INDEX_URL, "=".join([prev, marker])]) + res = self._test_index_paginated(marker=marker, sort_dir="asc", + volumes=expected_volumes, url=url, + has_more=True, has_prev=False) + volumes = res.context['volumes_table'].data + self.assertItemsEqual(volumes, expected_volumes) + @test.create_stubs({cinder: ('volume_create', 'volume_snapshot_list', 'volume_type_list', @@ -108,11 +266,11 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:create') + url = reverse('horizon:project:volumes:create') res = self.client.post(url, formData) self.assertNoFormErrors(res) - redirect_url = VOLUME_VOLUMES_TAB_URL + redirect_url = INDEX_URL self.assertRedirectsNoFollow(res, redirect_url) @test.create_stubs({cinder: ('volume_create', @@ -181,10 +339,10 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:create') + url = reverse('horizon:project:volumes:create') res = self.client.post(url, formData) - redirect_url = VOLUME_VOLUMES_TAB_URL + redirect_url = INDEX_URL self.assertRedirectsNoFollow(res, redirect_url) @test.create_stubs({cinder: ('volume_create', @@ -251,10 +409,10 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:create') + url = reverse('horizon:project:volumes:create') res = self.client.post(url, formData) - redirect_url = VOLUME_VOLUMES_TAB_URL + redirect_url = INDEX_URL self.assertRedirectsNoFollow(res, redirect_url) @test.create_stubs({cinder: ('volume_create', @@ -301,12 +459,12 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() # get snapshot from url - url = reverse('horizon:project:volumes:volumes:create') + url = reverse('horizon:project:volumes:create') res = self.client.post("?".join([url, "snapshot_id=" + str(snapshot.id)]), formData) - redirect_url = VOLUME_VOLUMES_TAB_URL + redirect_url = INDEX_URL self.assertRedirectsNoFollow(res, redirect_url) @test.create_stubs({cinder: ('volume_create', @@ -375,8 +533,8 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): source_volid=volume.id).AndReturn(volume) self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:create') - redirect_url = VOLUME_VOLUMES_TAB_URL + url = reverse('horizon:project:volumes:create') + redirect_url = INDEX_URL res = self.client.post(url, formData) self.assertNoFormErrors(res) self.assertMessageCount(info=1) @@ -451,10 +609,10 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() # get snapshot from dropdown list - url = reverse('horizon:project:volumes:volumes:create') + url = reverse('horizon:project:volumes:create') res = self.client.post(url, formData) - redirect_url = VOLUME_VOLUMES_TAB_URL + redirect_url = INDEX_URL self.assertRedirectsNoFollow(res, redirect_url) @test.create_stubs({cinder: ('volume_snapshot_get', @@ -497,7 +655,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:create') + url = reverse('horizon:project:volumes:create') res = self.client.post("?".join([url, "snapshot_id=" + str(snapshot.id)]), formData, follow=True) @@ -555,12 +713,12 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() # get image from url - url = reverse('horizon:project:volumes:volumes:create') + url = reverse('horizon:project:volumes:create') res = self.client.post("?".join([url, "image_id=" + str(image.id)]), formData) - redirect_url = VOLUME_VOLUMES_TAB_URL + redirect_url = INDEX_URL self.assertRedirectsNoFollow(res, redirect_url) @test.create_stubs({cinder: ('volume_create', @@ -632,10 +790,10 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() # get image from dropdown list - url = reverse('horizon:project:volumes:volumes:create') + url = reverse('horizon:project:volumes:create') res = self.client.post(url, formData) - redirect_url = VOLUME_VOLUMES_TAB_URL + redirect_url = INDEX_URL self.assertRedirectsNoFollow(res, redirect_url) @test.create_stubs({cinder: ('volume_type_list', @@ -683,7 +841,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:create') + url = reverse('horizon:project:volumes:create') res = self.client.post("?".join([url, "image_id=" + str(image.id)]), formData, follow=True) @@ -738,7 +896,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:create') + url = reverse('horizon:project:volumes:create') res = self.client.post("?".join([url, "image_id=" + str(image.id)]), formData, follow=True) @@ -835,7 +993,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:create') + url = reverse('horizon:project:volumes:create') res = self.client.post(url, formData) expected_error = [u'A volume of 5000GiB cannot be created as you only' @@ -914,7 +1072,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:create') + url = reverse('horizon:project:volumes:create') res = self.client.post(url, formData) expected_error = [u'You are already using all of your available' @@ -942,8 +1100,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): search_opts=None).\ AndReturn([]) cinder.volume_delete(IsA(http.HttpRequest), volume.id) - api.nova.server_list(IsA(http.HttpRequest), search_opts=None, - detailed=False).\ + api.nova.server_list(IsA(http.HttpRequest), search_opts=None).\ AndReturn([self.servers.list(), False]) cinder.volume_list_paged( IsA(http.HttpRequest), marker=None, paginate=True, sort_dir='desc', @@ -951,15 +1108,14 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): cinder.volume_snapshot_list(IsA(http.HttpRequest), search_opts=None).\ AndReturn([]) - api.nova.server_list(IsA(http.HttpRequest), search_opts=None, - detailed=False).\ + api.nova.server_list(IsA(http.HttpRequest), search_opts=None).\ AndReturn([self.servers.list(), False]) cinder.tenant_absolute_limits(IsA(http.HttpRequest)).MultipleTimes().\ AndReturn(self.cinder_limits['absolute']) self.mox.ReplayAll() - url = VOLUME_INDEX_URL + url = INDEX_URL res = self.client.post(url, formData, follow=True) self.assertIn("Scheduled deletion of Volume: Volume name", [m.message for m in res.context['messages']]) @@ -977,7 +1133,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() - url = (VOLUME_INDEX_URL + + url = (INDEX_URL + "?action=row_update&table=volumes&obj_id=" + volume.id) res = self.client.get(url, {}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') @@ -1005,7 +1161,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): api.nova.server_list(IsA(http.HttpRequest)).AndReturn([servers, False]) self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:attach', + url = reverse('horizon:project:volumes:attach', args=[volume.id]) res = self.client.get(url) msg = 'Volume %s on instance %s' % (volume.name, servers[0].name) @@ -1038,7 +1194,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): api.nova.server_list(IsA(http.HttpRequest)).AndReturn([servers, False]) self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:attach', + url = reverse('horizon:project:volumes:attach', args=[volume.id]) res = self.client.get(url) form = res.context['form'] @@ -1057,7 +1213,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): api.nova.server_list(IsA(http.HttpRequest)).AndReturn([servers, False]) self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:attach', + url = reverse('horizon:project:volumes:attach', args=[volume.id]) res = self.client.get(url) # Assert the device field is hidden. @@ -1080,7 +1236,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:attach', + url = reverse('horizon:project:volumes:attach', args=[volume.id]) res = self.client.get(url) @@ -1120,7 +1276,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): cinder.tenant_absolute_limits(IsA(http.HttpRequest)).AndReturn(limits) self.mox.ReplayAll() - res_url = (VOLUME_INDEX_URL + + res_url = (INDEX_URL + "?action=row_update&table=volumes&obj_id=" + volume.id) res = self.client.get(res_url, {}, @@ -1128,7 +1284,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): snapshot_action = self._get_volume_row_action_from_ajax( res, 'snapshots', volume.id) - self.assertEqual('horizon:project:volumes:volumes:create_snapshot', + self.assertEqual('horizon:project:volumes:create_snapshot', snapshot_action.url) self.assertEqual(set(['ajax-modal']), set(snapshot_action.classes)) self.assertEqual('Create Snapshot', @@ -1147,7 +1303,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): cinder.tenant_absolute_limits(IsA(http.HttpRequest)).AndReturn(limits) self.mox.ReplayAll() - res_url = (VOLUME_INDEX_URL + + res_url = (INDEX_URL + "?action=row_update&table=volumes&obj_id=" + volume.id) res = self.client.get(res_url, {}, @@ -1177,15 +1333,14 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): cinder.volume_snapshot_list(IsA(http.HttpRequest), search_opts=None).\ AndReturn([]) - api.nova.server_list(IsA(http.HttpRequest), search_opts=None, - detailed=False)\ + api.nova.server_list(IsA(http.HttpRequest), search_opts=None)\ .AndReturn([self.servers.list(), False]) cinder.tenant_absolute_limits(IsA(http.HttpRequest))\ .MultipleTimes().AndReturn(limits) self.mox.ReplayAll() - res = self.client.get(VOLUME_INDEX_URL) - self.assertTemplateUsed(res, 'project/volumes/index.html') + res = self.client.get(INDEX_URL) + self.assertTemplateUsed(res, 'horizon/common/_data_table_view.html') volumes = res.context['volumes_table'].data self.assertItemsEqual(volumes, self.cinder_volumes.list()) @@ -1196,7 +1351,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): set(create_action.classes)) self.assertEqual('Create Volume', six.text_type(create_action.verbose_name)) - self.assertEqual('horizon:project:volumes:volumes:create', + self.assertEqual('horizon:project:volumes:create', create_action.url) self.assertEqual((('volume', 'volume:create'),), create_action.policy_rules) @@ -1219,15 +1374,14 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): cinder.volume_snapshot_list(IsA(http.HttpRequest), search_opts=None).\ AndReturn([]) - api.nova.server_list(IsA(http.HttpRequest), search_opts=None, - detailed=False)\ + api.nova.server_list(IsA(http.HttpRequest), search_opts=None)\ .AndReturn([self.servers.list(), False]) cinder.tenant_absolute_limits(IsA(http.HttpRequest))\ .MultipleTimes().AndReturn(limits) self.mox.ReplayAll() - res = self.client.get(VOLUME_INDEX_URL) - self.assertTemplateUsed(res, 'project/volumes/index.html') + res = self.client.get(INDEX_URL) + self.assertTemplateUsed(res, 'horizon/common/_data_table_view.html') volumes = res.context['volumes_table'].data self.assertItemsEqual(volumes, self.cinder_volumes.list()) @@ -1257,7 +1411,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:detail', + url = reverse('horizon:project:volumes:detail', args=[volume.id]) res = self.client.get(url) @@ -1277,7 +1431,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:encryption_detail', + url = reverse('horizon:project:volumes:encryption_detail', args=[volume.id]) res = self.client.get(url) @@ -1305,7 +1459,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:encryption_detail', + url = reverse('horizon:project:volumes:encryption_detail', args=[volume.id]) res = self.client.get(url) @@ -1329,7 +1483,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() - url = (VOLUME_INDEX_URL + + url = (INDEX_URL + "?action=row_update&table=volumes&obj_id=" + volume.id) res = self.client.get(url, {}, @@ -1350,11 +1504,11 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:detail', + url = reverse('horizon:project:volumes:detail', args=[volume.id]) res = self.client.get(url) - self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL) + self.assertRedirectsNoFollow(res, INDEX_URL) @test.create_stubs({cinder: ('volume_update', 'volume_set_bootable', @@ -1378,10 +1532,10 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): 'description': volume.description, 'bootable': False} - url = reverse('horizon:project:volumes:volumes:update', + url = reverse('horizon:project:volumes:update', args=[volume.id]) res = self.client.post(url, formData) - self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL) + self.assertRedirectsNoFollow(res, INDEX_URL) @test.create_stubs({cinder: ('volume_update', 'volume_set_bootable', @@ -1405,10 +1559,10 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): 'description': volume.description, 'bootable': False} - url = reverse('horizon:project:volumes:volumes:update', + url = reverse('horizon:project:volumes:update', args=[volume.id]) res = self.client.post(url, formData) - self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL) + self.assertRedirectsNoFollow(res, INDEX_URL) @test.create_stubs({cinder: ('volume_update', 'volume_set_bootable', @@ -1432,10 +1586,10 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): 'description': 'update bootable flag', 'bootable': True} - url = reverse('horizon:project:volumes:volumes:update', + url = reverse('horizon:project:volumes:update', args=[volume.id]) res = self.client.post(url, formData) - self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL) + self.assertRedirectsNoFollow(res, INDEX_URL) @test.create_stubs({cinder: ('volume_upload_to_image', 'volume_get')}) @@ -1468,14 +1622,14 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:upload_to_image', + url = reverse('horizon:project:volumes:upload_to_image', args=[volume.id]) res = self.client.post(url, form_data) self.assertNoFormErrors(res) self.assertMessageCount(info=1) - redirect_url = VOLUME_INDEX_URL + redirect_url = INDEX_URL self.assertRedirectsNoFollow(res, redirect_url) @test.create_stubs({cinder: ('volume_get', @@ -1501,11 +1655,11 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:extend', + url = reverse('horizon:project:volumes:extend', args=[volume.id]) res = self.client.post(url, formData) - redirect_url = VOLUME_INDEX_URL + redirect_url = INDEX_URL self.assertRedirectsNoFollow(res, redirect_url) @test.create_stubs({cinder: ('volume_get',), @@ -1527,7 +1681,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:extend', + url = reverse('horizon:project:volumes:extend', args=[volume.id]) res = self.client.post(url, formData) self.assertFormErrors(res, 1, @@ -1546,7 +1700,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() - url = (VOLUME_INDEX_URL + + url = (INDEX_URL + "?action=row_update&table=volumes&obj_id=" + volume.id) res = self.client.get(url, {}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') @@ -1582,13 +1736,13 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:retype', + url = reverse('horizon:project:volumes:retype', args=[volume.id]) res = self.client.post(url, form_data) self.assertNoFormErrors(res) - redirect_url = VOLUME_INDEX_URL + redirect_url = INDEX_URL self.assertRedirectsNoFollow(res, redirect_url) def test_encryption_false(self): @@ -1618,15 +1772,14 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): cinder.volume_snapshot_list(IsA(http.HttpRequest), search_opts=None).\ AndReturn(self.cinder_volume_snapshots.list()) - api.nova.server_list(IsA(http.HttpRequest), search_opts=None, - detailed=False)\ + api.nova.server_list(IsA(http.HttpRequest), search_opts=None)\ .AndReturn([self.servers.list(), False]) cinder.tenant_absolute_limits(IsA(http.HttpRequest))\ .MultipleTimes('limits').AndReturn(limits) self.mox.ReplayAll() - res = self.client.get(VOLUME_INDEX_URL) + res = self.client.get(INDEX_URL) rows = res.context['volumes_table'].get_rows() if encryption: @@ -1656,7 +1809,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() - url = reverse('horizon:project:volumes:volumes:extend', + url = reverse('horizon:project:volumes:extend', args=[volume.id]) res = self.client.post(url, formData) self.assertFormError(res, "form", "new_size", @@ -1680,15 +1833,14 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): cinder.volume_snapshot_list(IsA(http.HttpRequest), search_opts=None).\ AndReturn([]) - api.nova.server_list(IsA(http.HttpRequest), search_opts=None, - detailed=False)\ + api.nova.server_list(IsA(http.HttpRequest), search_opts=None)\ .AndReturn([self.servers.list(), False]) cinder.tenant_absolute_limits(IsA(http.HttpRequest))\ .MultipleTimes().AndReturn(limits) self.mox.ReplayAll() - res = self.client.get(VOLUME_INDEX_URL) + res = self.client.get(INDEX_URL) table = res.context['volumes_table'] # Verify that the create transfer action is present if and only if @@ -1713,7 +1865,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() # Create a transfer for the first available volume - url = reverse('horizon:project:volumes:volumes:create_transfer', + url = reverse('horizon:project:volumes:create_transfer', args=[volToTransfer.id]) res = self.client.post(url, formData) self.assertNoFormErrors(res) @@ -1747,15 +1899,14 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): search_opts=None).\ AndReturn([]) cinder.transfer_delete(IsA(http.HttpRequest), transfer.id) - api.nova.server_list(IsA(http.HttpRequest), search_opts=None, - detailed=False).\ + api.nova.server_list(IsA(http.HttpRequest), search_opts=None).\ AndReturn([self.servers.list(), False]) cinder.tenant_absolute_limits(IsA(http.HttpRequest)).MultipleTimes().\ AndReturn(self.cinder_limits['absolute']) self.mox.ReplayAll() - url = VOLUME_INDEX_URL + url = INDEX_URL res = self.client.post(url, formData, follow=True) self.assertNoFormErrors(res) self.assertIn('Successfully deleted volume transfer "test transfer"', @@ -1770,7 +1921,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mox.ReplayAll() formData = {'transfer_id': transfer.id, 'auth_key': transfer.auth_key} - url = reverse('horizon:project:volumes:volumes:accept_transfer') + url = reverse('horizon:project:volumes:accept_transfer') res = self.client.post(url, formData, follow=True) self.assertNoFormErrors(res) @@ -1786,7 +1937,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): filename = "{}.txt".format(slugify(transfer.id)) - url = reverse('horizon:project:volumes:volumes:' + url = reverse('horizon:project:volumes:' 'download_transfer_creds', kwargs={'transfer_id': transfer.id, 'auth_key': transfer.auth_key}) diff --git a/openstack_dashboard/dashboards/project/volumes/urls.py b/openstack_dashboard/dashboards/project/volumes/urls.py index ef6a3f9e23..c750d6aa69 100644 --- a/openstack_dashboard/dashboards/project/volumes/urls.py +++ b/openstack_dashboard/dashboards/project/volumes/urls.py @@ -12,18 +12,52 @@ # License for the specific language governing permissions and limitations # under the License. -from django.conf.urls import include from django.conf.urls import url +from openstack_dashboard.dashboards.project.backups \ + import views as backup_views from openstack_dashboard.dashboards.project.volumes import views -from openstack_dashboard.dashboards.project.volumes.volumes \ - import urls as volume_urls urlpatterns = [ - url(r'^$', views.IndexView.as_view(), name='index'), - url(r'^\?tab=volumes_and_snapshots__volumes_tab$', - views.IndexView.as_view(), name='volumes_tab'), - url(r'', include( - volume_urls, - namespace='volumes')), + url(r'^$', views.VolumesView.as_view(), name='index'), + url(r'^create/$', views.CreateView.as_view(), name='create'), + url(r'^(?P[^/]+)/extend/$', + views.ExtendView.as_view(), + name='extend'), + url(r'^(?P[^/]+)/attach/$', + views.EditAttachmentsView.as_view(), + name='attach'), + url(r'^(?P[^/]+)/create_snapshot/$', + views.CreateSnapshotView.as_view(), + name='create_snapshot'), + url(r'^(?P[^/]+)/create_transfer/$', + views.CreateTransferView.as_view(), + name='create_transfer'), + url(r'^accept_transfer/$', + views.AcceptTransferView.as_view(), + name='accept_transfer'), + url(r'^(?P[^/]+)/auth/(?P[^/]+)/$', + views.ShowTransferView.as_view(), + name='show_transfer'), + url(r'^(?P[^/]+)/create_backup/$', + backup_views.CreateBackupView.as_view(), + name='create_backup'), + url(r'^(?P[^/]+)/$', + views.DetailView.as_view(), + name='detail'), + url(r'^(?P[^/]+)/upload_to_image/$', + views.UploadToImageView.as_view(), + name='upload_to_image'), + url(r'^(?P[^/]+)/update/$', + views.UpdateView.as_view(), + name='update'), + url(r'^(?P[^/]+)/retype/$', + views.RetypeView.as_view(), + name='retype'), + url(r'^(?P[^/]+)/encryption_detail/$', + views.EncryptionDetailView.as_view(), + name='encryption_detail'), + url(r'^(?P[^/]+)/download_creds/(?P[^/]+)$', + views.DownloadTransferCreds.as_view(), + name='download_transfer_creds'), ] diff --git a/openstack_dashboard/dashboards/project/volumes/views.py b/openstack_dashboard/dashboards/project/volumes/views.py index 81e9fa732a..777ba29da3 100644 --- a/openstack_dashboard/dashboards/project/volumes/views.py +++ b/openstack_dashboard/dashboards/project/volumes/views.py @@ -12,15 +12,626 @@ # License for the specific language governing permissions and limitations # under the License. +""" +Views for managing volumes. +""" + +from collections import OrderedDict +import json + +from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse_lazy +from django import http +from django.template.defaultfilters import slugify # noqa +from django.utils.decorators import method_decorator +from django.utils import encoding from django.utils.translation import ugettext_lazy as _ +from django.views.decorators.cache import cache_control +from django.views.decorators.cache import never_cache +from django.views import generic +from horizon import exceptions +from horizon import forms +from horizon import tables from horizon import tabs +from horizon.utils import memoized +from openstack_dashboard.api import cinder +from openstack_dashboard.api import nova +from openstack_dashboard import exceptions as dashboard_exception +from openstack_dashboard.usage import quotas +from openstack_dashboard.utils import filters + +from openstack_dashboard.dashboards.project.volumes \ + import forms as volume_forms +from openstack_dashboard.dashboards.project.volumes \ + import tables as volume_tables from openstack_dashboard.dashboards.project.volumes \ import tabs as project_tabs -class IndexView(tabs.TabbedTableView): - tab_group_class = project_tabs.VolumeAndSnapshotTabs - template_name = 'project/volumes/index.html' +class VolumeTableMixIn(object): + _has_more_data = False + _has_prev_data = False + + def _get_volumes(self, search_opts=None): + try: + marker, sort_dir = self._get_marker() + volumes, self._has_more_data, self._has_prev_data = \ + cinder.volume_list_paged(self.request, marker=marker, + search_opts=search_opts, + sort_dir=sort_dir, paginate=True) + + if sort_dir == "asc": + volumes.reverse() + + return volumes + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve volume list.')) + return [] + + def _get_instances(self, search_opts=None, instance_ids=None): + if not instance_ids: + return [] + try: + # TODO(tsufiev): we should pass attached_instance_ids to + # nova.server_list as soon as Nova API allows for this + instances, has_more = nova.server_list(self.request, + search_opts=search_opts) + return instances + except Exception: + exceptions.handle(self.request, + _("Unable to retrieve volume/instance " + "attachment information")) + return [] + + def _get_volumes_ids_with_snapshots(self, search_opts=None): + try: + volume_ids = [] + snapshots = cinder.volume_snapshot_list( + self.request, search_opts=search_opts) + if snapshots: + # extract out the volume ids + volume_ids = set([(s.volume_id) for s in snapshots]) + except Exception: + exceptions.handle(self.request, + _("Unable to retrieve snapshot list.")) + + return volume_ids + + def _get_attached_instance_ids(self, volumes): + attached_instance_ids = [] + for volume in volumes: + for att in volume.attachments: + server_id = att.get('server_id', None) + if server_id is not None: + attached_instance_ids.append(server_id) + return attached_instance_ids + + # set attachment string and if volume has snapshots + def _set_volume_attributes(self, + volumes, + instances, + volume_ids_with_snapshots): + instances = OrderedDict([(inst.id, inst) for inst in instances]) + for volume in volumes: + if volume_ids_with_snapshots: + if volume.id in volume_ids_with_snapshots: + setattr(volume, 'has_snapshot', True) + if instances: + for att in volume.attachments: + server_id = att.get('server_id', None) + att['instance'] = instances.get(server_id, None) + + +class VolumesView(tables.PagedTableMixin, VolumeTableMixIn, + tables.DataTableView): + table_class = volume_tables.VolumesTable page_title = _("Volumes") + + def get_data(self): + volumes = self._get_volumes() + attached_instance_ids = self._get_attached_instance_ids(volumes) + instances = self._get_instances(instance_ids=attached_instance_ids) + volume_ids_with_snapshots = self._get_volumes_ids_with_snapshots() + self._set_volume_attributes( + volumes, instances, volume_ids_with_snapshots) + return volumes + + +class DetailView(tabs.TabView): + tab_group_class = project_tabs.VolumeDetailTabs + template_name = 'horizon/common/_detail.html' + page_title = "{{ volume.name|default:volume.id }}" + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + volume = self.get_data() + table = volume_tables.VolumesTable(self.request) + context["volume"] = volume + context["url"] = self.get_redirect_url() + context["actions"] = table.render_row_actions(volume) + choices = volume_tables.VolumesTableBase.STATUS_DISPLAY_CHOICES + volume.status_label = filters.get_display_label(choices, volume.status) + return context + + @memoized.memoized_method + def get_data(self): + try: + volume_id = self.kwargs['volume_id'] + volume = cinder.volume_get(self.request, volume_id) + snapshots = cinder.volume_snapshot_list( + self.request, search_opts={'volume_id': volume.id}) + if snapshots: + setattr(volume, 'has_snapshot', True) + for att in volume.attachments: + att['instance'] = nova.server_get(self.request, + att['server_id']) + except Exception: + redirect = self.get_redirect_url() + exceptions.handle(self.request, + _('Unable to retrieve volume details.'), + redirect=redirect) + return volume + + def get_redirect_url(self): + return reverse('horizon:project:volumes:index') + + def get_tabs(self, request, *args, **kwargs): + volume = self.get_data() + return self.tab_group_class(request, volume=volume, **kwargs) + + +class CreateView(forms.ModalFormView): + form_class = volume_forms.CreateForm + template_name = 'project/volumes/create.html' + submit_label = _("Create Volume") + submit_url = reverse_lazy("horizon:project:volumes:create") + success_url = reverse_lazy('horizon:project:volumes:index') + page_title = _("Create Volume") + + def get_initial(self): + initial = super(CreateView, self).get_initial() + self.default_vol_type = None + try: + self.default_vol_type = cinder.volume_type_default(self.request) + initial['type'] = self.default_vol_type.name + except dashboard_exception.NOT_FOUND: + pass + return initial + + def get_context_data(self, **kwargs): + context = super(CreateView, self).get_context_data(**kwargs) + try: + context['usages'] = quotas.tenant_limit_usages(self.request) + context['volume_types'] = self._get_volume_types() + except Exception: + exceptions.handle(self.request) + return context + + def _get_volume_types(self): + volume_types = [] + try: + volume_types = cinder.volume_type_list(self.request) + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve volume type list.')) + + # check if we have default volume type so we can present the + # description of no volume type differently + no_type_description = None + if self.default_vol_type is None: + message = \ + _("If \"No volume type\" is selected, the volume will be " + "created without a volume type.") + + no_type_description = encoding.force_text(message) + + type_descriptions = [{'name': '', + 'description': no_type_description}] + \ + [{'name': type.name, + 'description': getattr(type, "description", "")} + for type in volume_types] + + return json.dumps(type_descriptions) + + +class ExtendView(forms.ModalFormView): + form_class = volume_forms.ExtendForm + template_name = 'project/volumes/extend.html' + submit_label = _("Extend Volume") + submit_url = "horizon:project:volumes:extend" + success_url = reverse_lazy("horizon:project:volumes:index") + page_title = _("Extend Volume") + + def get_object(self): + if not hasattr(self, "_object"): + volume_id = self.kwargs['volume_id'] + try: + self._object = cinder.volume_get(self.request, volume_id) + except Exception: + self._object = None + exceptions.handle(self.request, + _('Unable to retrieve volume information.')) + return self._object + + def get_context_data(self, **kwargs): + context = super(ExtendView, self).get_context_data(**kwargs) + context['volume'] = self.get_object() + args = (self.kwargs['volume_id'],) + context['submit_url'] = reverse(self.submit_url, args=args) + try: + usages = quotas.tenant_limit_usages(self.request) + usages['gigabytesUsed'] = (usages['gigabytesUsed'] + - context['volume'].size) + context['usages'] = usages + except Exception: + exceptions.handle(self.request) + return context + + def get_initial(self): + volume = self.get_object() + return {'id': self.kwargs['volume_id'], + 'name': volume.name, + 'orig_size': volume.size} + + +class CreateSnapshotView(forms.ModalFormView): + form_class = volume_forms.CreateSnapshotForm + template_name = 'project/volumes/create_snapshot.html' + submit_url = "horizon:project:volumes:create_snapshot" + success_url = reverse_lazy('horizon:project:snapshots:index') + page_title = _("Create Volume Snapshot") + + def get_context_data(self, **kwargs): + context = super(CreateSnapshotView, self).get_context_data(**kwargs) + context['volume_id'] = self.kwargs['volume_id'] + args = (self.kwargs['volume_id'],) + context['submit_url'] = reverse(self.submit_url, args=args) + try: + volume = cinder.volume_get(self.request, context['volume_id']) + if (volume.status == 'in-use'): + context['attached'] = True + context['form'].set_warning(_("This volume is currently " + "attached to an instance. " + "In some cases, creating a " + "snapshot from an attached " + "volume can result in a " + "corrupted snapshot.")) + context['usages'] = quotas.tenant_limit_usages(self.request) + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve volume information.')) + return context + + def get_initial(self): + return {'volume_id': self.kwargs["volume_id"]} + + +class UploadToImageView(forms.ModalFormView): + form_class = volume_forms.UploadToImageForm + template_name = 'project/volumes/upload_to_image.html' + submit_label = _("Upload") + submit_url = "horizon:project:volumes:upload_to_image" + success_url = reverse_lazy("horizon:project:volumes:index") + page_title = _("Upload Volume to Image") + + @memoized.memoized_method + def get_data(self): + try: + volume_id = self.kwargs['volume_id'] + volume = cinder.volume_get(self.request, volume_id) + except Exception: + error_message = _( + 'Unable to retrieve volume information for volume: "%s"') \ + % volume_id + exceptions.handle(self.request, + error_message, + redirect=self.success_url) + + return volume + + def get_context_data(self, **kwargs): + context = super(UploadToImageView, self).get_context_data(**kwargs) + context['volume'] = self.get_data() + args = (self.kwargs['volume_id'],) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + volume = self.get_data() + + return {'id': self.kwargs['volume_id'], + 'name': volume.name, + 'status': volume.status} + + +class CreateTransferView(forms.ModalFormView): + form_class = volume_forms.CreateTransferForm + template_name = 'project/volumes/create_transfer.html' + success_url = reverse_lazy('horizon:project:volumes:index') + modal_id = "create_volume_transfer_modal" + submit_label = _("Create Volume Transfer") + submit_url = "horizon:project:volumes:create_transfer" + page_title = _("Create Volume Transfer") + + def get_context_data(self, *args, **kwargs): + context = super(CreateTransferView, self).get_context_data(**kwargs) + volume_id = self.kwargs['volume_id'] + context['volume_id'] = volume_id + context['submit_url'] = reverse(self.submit_url, args=[volume_id]) + return context + + def get_initial(self): + return {'volume_id': self.kwargs["volume_id"]} + + +class AcceptTransferView(forms.ModalFormView): + form_class = volume_forms.AcceptTransferForm + template_name = 'project/volumes/accept_transfer.html' + success_url = reverse_lazy('horizon:project:volumes:index') + modal_id = "accept_volume_transfer_modal" + submit_label = _("Accept Volume Transfer") + submit_url = reverse_lazy( + "horizon:project:volumes:accept_transfer") + page_title = _("Accept Volume Transfer") + + +class ShowTransferView(forms.ModalFormView): + form_class = volume_forms.ShowTransferForm + template_name = 'project/volumes/show_transfer.html' + success_url = reverse_lazy('horizon:project:volumes:index') + modal_id = "show_volume_transfer_modal" + submit_url = "horizon:project:volumes:show_transfer" + cancel_label = _("Close") + download_label = _("Download transfer credentials") + page_title = _("Volume Transfer Details") + + def get_object(self): + try: + return self._object + except AttributeError: + transfer_id = self.kwargs['transfer_id'] + try: + self._object = cinder.transfer_get(self.request, transfer_id) + return self._object + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve volume transfer.')) + + def get_context_data(self, **kwargs): + context = super(ShowTransferView, self).get_context_data(**kwargs) + context['transfer_id'] = self.kwargs['transfer_id'] + context['auth_key'] = self.kwargs['auth_key'] + context['submit_url'] = reverse(self.submit_url, args=[ + context['transfer_id'], context['auth_key']]) + context['download_label'] = self.download_label + context['download_url'] = reverse( + 'horizon:project:volumes:download_transfer_creds', + args=[context['transfer_id'], context['auth_key']] + ) + return context + + def get_initial(self): + transfer = self.get_object() + return {'id': transfer.id, + 'name': transfer.name, + 'auth_key': self.kwargs['auth_key']} + + +class UpdateView(forms.ModalFormView): + form_class = volume_forms.UpdateForm + modal_id = "update_volume_modal" + template_name = 'project/volumes/update.html' + submit_url = "horizon:project:volumes:update" + success_url = reverse_lazy("horizon:project:volumes:index") + page_title = _("Edit Volume") + + def get_object(self): + if not hasattr(self, "_object"): + vol_id = self.kwargs['volume_id'] + try: + self._object = cinder.volume_get(self.request, vol_id) + except Exception: + msg = _('Unable to retrieve volume.') + url = reverse('horizon:project:volumes:index') + exceptions.handle(self.request, msg, redirect=url) + return self._object + + def get_context_data(self, **kwargs): + context = super(UpdateView, self).get_context_data(**kwargs) + context['volume'] = self.get_object() + args = (self.kwargs['volume_id'],) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + volume = self.get_object() + return {'volume_id': self.kwargs["volume_id"], + 'name': volume.name, + 'description': volume.description, + 'bootable': volume.is_bootable} + + +class EditAttachmentsView(tables.DataTableView, forms.ModalFormView): + table_class = volume_tables.AttachmentsTable + form_class = volume_forms.AttachForm + form_id = "attach_volume_form" + modal_id = "attach_volume_modal" + template_name = 'project/volumes/attach.html' + submit_url = "horizon:project:volumes:attach" + success_url = reverse_lazy("horizon:project:volumes:index") + page_title = _("Manage Volume Attachments") + + @memoized.memoized_method + def get_object(self): + volume_id = self.kwargs['volume_id'] + try: + return cinder.volume_get(self.request, volume_id) + except Exception: + self._object = None + exceptions.handle(self.request, + _('Unable to retrieve volume information.')) + + def get_data(self): + attachments = [] + volume = self.get_object() + if volume is not None: + for att in volume.attachments: + att['volume_name'] = getattr(volume, 'name', att['device']) + attachments.append(att) + return attachments + + def get_initial(self): + try: + instances, has_more = nova.server_list(self.request) + except Exception: + instances = [] + exceptions.handle(self.request, + _("Unable to retrieve attachment information.")) + return {'volume': self.get_object(), + 'instances': instances} + + @memoized.memoized_method + def get_form(self, **kwargs): + form_class = kwargs.get('form_class', self.get_form_class()) + return super(EditAttachmentsView, self).get_form(form_class) + + def get_context_data(self, **kwargs): + context = super(EditAttachmentsView, self).get_context_data(**kwargs) + context['form'] = self.get_form() + volume = self.get_object() + args = (self.kwargs['volume_id'],) + context['submit_url'] = reverse(self.submit_url, args=args) + if volume and volume.status == 'available': + context['show_attach'] = True + else: + context['show_attach'] = False + context['volume'] = volume + if self.request.is_ajax(): + context['hide'] = True + return context + + def get(self, request, *args, **kwargs): + # Table action handling + handled = self.construct_tables() + if handled: + return handled + return self.render_to_response(self.get_context_data(**kwargs)) + + def post(self, request, *args, **kwargs): + form = self.get_form() + if form.is_valid(): + return self.form_valid(form) + else: + return self.get(request, *args, **kwargs) + + +class RetypeView(forms.ModalFormView): + form_class = volume_forms.RetypeForm + modal_id = "retype_volume_modal" + template_name = 'project/volumes/retype.html' + submit_label = _("Change Volume Type") + submit_url = "horizon:project:volumes:retype" + success_url = reverse_lazy("horizon:project:volumes:index") + page_title = _("Change Volume Type") + + @memoized.memoized_method + def get_data(self): + try: + volume_id = self.kwargs['volume_id'] + volume = cinder.volume_get(self.request, volume_id) + except Exception: + error_message = _( + 'Unable to retrieve volume information for volume: "%s"') \ + % volume_id + exceptions.handle(self.request, + error_message, + redirect=self.success_url) + + return volume + + def get_context_data(self, **kwargs): + context = super(RetypeView, self).get_context_data(**kwargs) + context['volume'] = self.get_data() + args = (self.kwargs['volume_id'],) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + volume = self.get_data() + + return {'id': self.kwargs['volume_id'], + 'name': volume.name, + 'volume_type': volume.volume_type} + + +class EncryptionDetailView(generic.TemplateView): + template_name = 'project/volumes/encryption_detail.html' + page_title = _("Volume Encryption Details: {{ volume.name }}") + + def get_context_data(self, **kwargs): + context = super(EncryptionDetailView, self).get_context_data(**kwargs) + volume = self.get_volume_data() + context["encryption_metadata"] = self.get_encryption_data() + context["volume"] = volume + context["page_title"] = _("Volume Encryption Details: " + "%(volume_name)s") % {'volume_name': + volume.name} + return context + + @memoized.memoized_method + def get_encryption_data(self): + try: + volume_id = self.kwargs['volume_id'] + self._encryption_metadata = \ + cinder.volume_get_encryption_metadata(self.request, + volume_id) + except Exception: + redirect = self.get_redirect_url() + exceptions.handle(self.request, + _('Unable to retrieve volume encryption ' + 'details.'), + redirect=redirect) + return self._encryption_metadata + + @memoized.memoized_method + def get_volume_data(self): + try: + volume_id = self.kwargs['volume_id'] + volume = cinder.volume_get(self.request, volume_id) + except Exception: + redirect = self.get_redirect_url() + exceptions.handle(self.request, + _('Unable to retrieve volume details.'), + redirect=redirect) + return volume + + def get_redirect_url(self): + return reverse('horizon:project:volumes:index') + + +class DownloadTransferCreds(generic.View): + # TODO(Itxaka): Remove cache_control in django >= 1.9 + # https://code.djangoproject.com/ticket/13008 + @method_decorator(cache_control(max_age=0, no_cache=True, + no_store=True, must_revalidate=True)) + @method_decorator(never_cache) + def get(self, request, transfer_id, auth_key): + try: + transfer = cinder.transfer_get(self.request, transfer_id) + except Exception: + transfer = None + response = http.HttpResponse(content_type='application/text') + response['Content-Disposition'] = \ + 'attachment; filename=%s.txt' % slugify(transfer_id) + response.write('%s: %s\n%s: %s\n%s: %s' % ( + _("Transfer name"), + getattr(transfer, 'name', ''), + _("Transfer ID"), + transfer_id, + _("Authorization Key"), + auth_key)) + response['Content-Length'] = str(len(response.content)) + return response diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/__init__.py b/openstack_dashboard/dashboards/project/volumes/volumes/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/tabs.py b/openstack_dashboard/dashboards/project/volumes/volumes/tabs.py deleted file mode 100644 index 919212e1ec..0000000000 --- a/openstack_dashboard/dashboards/project/volumes/volumes/tabs.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2012 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from django.utils.translation import ugettext_lazy as _ - -from horizon import tabs - - -class OverviewTab(tabs.Tab): - name = _("Overview") - slug = "overview" - template_name = ("project/volumes/volumes/_detail_overview.html") - - def get_context_data(self, request): - return {"volume": self.tab_group.kwargs['volume']} - - -class VolumeDetailTabs(tabs.TabGroup): - slug = "volume_details" - tabs = (OverviewTab,) diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/urls.py b/openstack_dashboard/dashboards/project/volumes/volumes/urls.py deleted file mode 100644 index 30b96c476b..0000000000 --- a/openstack_dashboard/dashboards/project/volumes/volumes/urls.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright 2012 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from django.conf.urls import url - -from openstack_dashboard.dashboards.project.volumes \ - .volumes import views -from openstack_dashboard.dashboards.project.backups \ - import views as backup_views - - -urlpatterns = [ - url(r'^create/$', views.CreateView.as_view(), name='create'), - url(r'^(?P[^/]+)/extend/$', - views.ExtendView.as_view(), - name='extend'), - url(r'^(?P[^/]+)/attach/$', - views.EditAttachmentsView.as_view(), - name='attach'), - url(r'^(?P[^/]+)/create_snapshot/$', - views.CreateSnapshotView.as_view(), - name='create_snapshot'), - url(r'^(?P[^/]+)/create_transfer/$', - views.CreateTransferView.as_view(), - name='create_transfer'), - url(r'^accept_transfer/$', - views.AcceptTransferView.as_view(), - name='accept_transfer'), - url(r'^(?P[^/]+)/auth/(?P[^/]+)/$', - views.ShowTransferView.as_view(), - name='show_transfer'), - url(r'^(?P[^/]+)/create_backup/$', - backup_views.CreateBackupView.as_view(), - name='create_backup'), - url(r'^(?P[^/]+)/$', - views.DetailView.as_view(), - name='detail'), - url(r'^(?P[^/]+)/upload_to_image/$', - views.UploadToImageView.as_view(), - name='upload_to_image'), - url(r'^(?P[^/]+)/update/$', - views.UpdateView.as_view(), - name='update'), - url(r'^(?P[^/]+)/retype/$', - views.RetypeView.as_view(), - name='retype'), - url(r'^(?P[^/]+)/encryption_detail/$', - views.EncryptionDetailView.as_view(), - name='encryption_detail'), - url(r'^(?P[^/]+)/download_creds/(?P[^/]+)$', - views.DownloadTransferCreds.as_view(), - name='download_transfer_creds'), -] diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/views.py b/openstack_dashboard/dashboards/project/volumes/volumes/views.py deleted file mode 100644 index 761f94fb47..0000000000 --- a/openstack_dashboard/dashboards/project/volumes/volumes/views.py +++ /dev/null @@ -1,547 +0,0 @@ -# Copyright 2012 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Views for managing volumes. -""" - -import json - -from django.core.urlresolvers import reverse -from django.core.urlresolvers import reverse_lazy -from django import http -from django.template.defaultfilters import slugify # noqa -from django.utils.decorators import method_decorator -from django.utils import encoding -from django.utils.translation import ugettext_lazy as _ -from django.views.decorators.cache import cache_control -from django.views.decorators.cache import never_cache -from django.views import generic - -from horizon import exceptions -from horizon import forms -from horizon import tables -from horizon import tabs -from horizon.utils import memoized - -from openstack_dashboard import api -from openstack_dashboard.api import cinder -from openstack_dashboard import exceptions as dashboard_exception -from openstack_dashboard.usage import quotas -from openstack_dashboard.utils import filters - -from openstack_dashboard.dashboards.project.volumes \ - .volumes import forms as project_forms - -from openstack_dashboard.dashboards.project.volumes \ - .volumes import tables as project_tables -from openstack_dashboard.dashboards.project.volumes \ - .volumes import tabs as project_tabs - - -class DetailView(tabs.TabView): - tab_group_class = project_tabs.VolumeDetailTabs - template_name = 'horizon/common/_detail.html' - page_title = "{{ volume.name|default:volume.id }}" - - def get_context_data(self, **kwargs): - context = super(DetailView, self).get_context_data(**kwargs) - volume = self.get_data() - table = project_tables.VolumesTable(self.request) - context["volume"] = volume - context["url"] = self.get_redirect_url() - context["actions"] = table.render_row_actions(volume) - choices = project_tables.VolumesTableBase.STATUS_DISPLAY_CHOICES - volume.status_label = filters.get_display_label(choices, volume.status) - return context - - @memoized.memoized_method - def get_data(self): - try: - volume_id = self.kwargs['volume_id'] - volume = cinder.volume_get(self.request, volume_id) - snapshots = cinder.volume_snapshot_list( - self.request, search_opts={'volume_id': volume.id}) - if snapshots: - setattr(volume, 'has_snapshot', True) - for att in volume.attachments: - att['instance'] = api.nova.server_get(self.request, - att['server_id']) - except Exception: - redirect = self.get_redirect_url() - exceptions.handle(self.request, - _('Unable to retrieve volume details.'), - redirect=redirect) - return volume - - def get_redirect_url(self): - return reverse('horizon:project:volumes:index') - - def get_tabs(self, request, *args, **kwargs): - volume = self.get_data() - return self.tab_group_class(request, volume=volume, **kwargs) - - -class CreateView(forms.ModalFormView): - form_class = project_forms.CreateForm - template_name = 'project/volumes/volumes/create.html' - submit_label = _("Create Volume") - submit_url = reverse_lazy("horizon:project:volumes:volumes:create") - success_url = reverse_lazy('horizon:project:volumes:volumes_tab') - page_title = _("Create Volume") - - def get_initial(self): - initial = super(CreateView, self).get_initial() - self.default_vol_type = None - try: - self.default_vol_type = cinder.volume_type_default(self.request) - initial['type'] = self.default_vol_type.name - except dashboard_exception.NOT_FOUND: - pass - return initial - - def get_context_data(self, **kwargs): - context = super(CreateView, self).get_context_data(**kwargs) - try: - context['usages'] = quotas.tenant_limit_usages(self.request) - context['volume_types'] = self._get_volume_types() - except Exception: - exceptions.handle(self.request) - return context - - def _get_volume_types(self): - volume_types = [] - try: - volume_types = cinder.volume_type_list(self.request) - except Exception: - exceptions.handle(self.request, - _('Unable to retrieve volume type list.')) - - # check if we have default volume type so we can present the - # description of no volume type differently - no_type_description = None - if self.default_vol_type is None: - message = \ - _("If \"No volume type\" is selected, the volume will be " - "created without a volume type.") - - no_type_description = encoding.force_text(message) - - type_descriptions = [{'name': '', - 'description': no_type_description}] + \ - [{'name': type.name, - 'description': getattr(type, "description", "")} - for type in volume_types] - - return json.dumps(type_descriptions) - - -class ExtendView(forms.ModalFormView): - form_class = project_forms.ExtendForm - template_name = 'project/volumes/volumes/extend.html' - submit_label = _("Extend Volume") - submit_url = "horizon:project:volumes:volumes:extend" - success_url = reverse_lazy("horizon:project:volumes:index") - page_title = _("Extend Volume") - - def get_object(self): - if not hasattr(self, "_object"): - volume_id = self.kwargs['volume_id'] - try: - self._object = cinder.volume_get(self.request, volume_id) - except Exception: - self._object = None - exceptions.handle(self.request, - _('Unable to retrieve volume information.')) - return self._object - - def get_context_data(self, **kwargs): - context = super(ExtendView, self).get_context_data(**kwargs) - context['volume'] = self.get_object() - args = (self.kwargs['volume_id'],) - context['submit_url'] = reverse(self.submit_url, args=args) - try: - usages = quotas.tenant_limit_usages(self.request) - usages['gigabytesUsed'] = (usages['gigabytesUsed'] - - context['volume'].size) - context['usages'] = usages - except Exception: - exceptions.handle(self.request) - return context - - def get_initial(self): - volume = self.get_object() - return {'id': self.kwargs['volume_id'], - 'name': volume.name, - 'orig_size': volume.size} - - -class CreateSnapshotView(forms.ModalFormView): - form_class = project_forms.CreateSnapshotForm - template_name = 'project/volumes/volumes/create_snapshot.html' - submit_url = "horizon:project:volumes:volumes:create_snapshot" - success_url = reverse_lazy('horizon:project:snapshots:index') - page_title = _("Create Volume Snapshot") - - def get_context_data(self, **kwargs): - context = super(CreateSnapshotView, self).get_context_data(**kwargs) - context['volume_id'] = self.kwargs['volume_id'] - args = (self.kwargs['volume_id'],) - context['submit_url'] = reverse(self.submit_url, args=args) - try: - volume = cinder.volume_get(self.request, context['volume_id']) - if (volume.status == 'in-use'): - context['attached'] = True - context['form'].set_warning(_("This volume is currently " - "attached to an instance. " - "In some cases, creating a " - "snapshot from an attached " - "volume can result in a " - "corrupted snapshot.")) - context['usages'] = quotas.tenant_limit_usages(self.request) - except Exception: - exceptions.handle(self.request, - _('Unable to retrieve volume information.')) - return context - - def get_initial(self): - return {'volume_id': self.kwargs["volume_id"]} - - -class UploadToImageView(forms.ModalFormView): - form_class = project_forms.UploadToImageForm - template_name = 'project/volumes/volumes/upload_to_image.html' - submit_label = _("Upload") - submit_url = "horizon:project:volumes:volumes:upload_to_image" - success_url = reverse_lazy("horizon:project:volumes:index") - page_title = _("Upload Volume to Image") - - @memoized.memoized_method - def get_data(self): - try: - volume_id = self.kwargs['volume_id'] - volume = cinder.volume_get(self.request, volume_id) - except Exception: - error_message = _( - 'Unable to retrieve volume information for volume: "%s"') \ - % volume_id - exceptions.handle(self.request, - error_message, - redirect=self.success_url) - - return volume - - def get_context_data(self, **kwargs): - context = super(UploadToImageView, self).get_context_data(**kwargs) - context['volume'] = self.get_data() - args = (self.kwargs['volume_id'],) - context['submit_url'] = reverse(self.submit_url, args=args) - return context - - def get_initial(self): - volume = self.get_data() - - return {'id': self.kwargs['volume_id'], - 'name': volume.name, - 'status': volume.status} - - -class CreateTransferView(forms.ModalFormView): - form_class = project_forms.CreateTransferForm - template_name = 'project/volumes/volumes/create_transfer.html' - success_url = reverse_lazy('horizon:project:volumes:volumes_tab') - modal_id = "create_volume_transfer_modal" - submit_label = _("Create Volume Transfer") - submit_url = "horizon:project:volumes:volumes:create_transfer" - page_title = _("Create Volume Transfer") - - def get_context_data(self, *args, **kwargs): - context = super(CreateTransferView, self).get_context_data(**kwargs) - volume_id = self.kwargs['volume_id'] - context['volume_id'] = volume_id - context['submit_url'] = reverse(self.submit_url, args=[volume_id]) - return context - - def get_initial(self): - return {'volume_id': self.kwargs["volume_id"]} - - -class AcceptTransferView(forms.ModalFormView): - form_class = project_forms.AcceptTransferForm - template_name = 'project/volumes/volumes/accept_transfer.html' - success_url = reverse_lazy('horizon:project:volumes:volumes_tab') - modal_id = "accept_volume_transfer_modal" - submit_label = _("Accept Volume Transfer") - submit_url = reverse_lazy( - "horizon:project:volumes:volumes:accept_transfer") - page_title = _("Accept Volume Transfer") - - -class ShowTransferView(forms.ModalFormView): - form_class = project_forms.ShowTransferForm - template_name = 'project/volumes/volumes/show_transfer.html' - success_url = reverse_lazy('horizon:project:volumes:volumes_tab') - modal_id = "show_volume_transfer_modal" - submit_url = "horizon:project:volumes:volumes:show_transfer" - cancel_label = _("Close") - download_label = _("Download transfer credentials") - page_title = _("Volume Transfer Details") - - def get_object(self): - try: - return self._object - except AttributeError: - transfer_id = self.kwargs['transfer_id'] - try: - self._object = cinder.transfer_get(self.request, transfer_id) - return self._object - except Exception: - exceptions.handle(self.request, - _('Unable to retrieve volume transfer.')) - - def get_context_data(self, **kwargs): - context = super(ShowTransferView, self).get_context_data(**kwargs) - context['transfer_id'] = self.kwargs['transfer_id'] - context['auth_key'] = self.kwargs['auth_key'] - context['submit_url'] = reverse(self.submit_url, args=[ - context['transfer_id'], context['auth_key']]) - context['download_label'] = self.download_label - context['download_url'] = reverse( - 'horizon:project:volumes:volumes:download_transfer_creds', - args=[context['transfer_id'], context['auth_key']] - ) - return context - - def get_initial(self): - transfer = self.get_object() - return {'id': transfer.id, - 'name': transfer.name, - 'auth_key': self.kwargs['auth_key']} - - -class UpdateView(forms.ModalFormView): - form_class = project_forms.UpdateForm - modal_id = "update_volume_modal" - template_name = 'project/volumes/volumes/update.html' - submit_url = "horizon:project:volumes:volumes:update" - success_url = reverse_lazy("horizon:project:volumes:index") - page_title = _("Edit Volume") - - def get_object(self): - if not hasattr(self, "_object"): - vol_id = self.kwargs['volume_id'] - try: - self._object = cinder.volume_get(self.request, vol_id) - except Exception: - msg = _('Unable to retrieve volume.') - url = reverse('horizon:project:volumes:index') - exceptions.handle(self.request, msg, redirect=url) - return self._object - - def get_context_data(self, **kwargs): - context = super(UpdateView, self).get_context_data(**kwargs) - context['volume'] = self.get_object() - args = (self.kwargs['volume_id'],) - context['submit_url'] = reverse(self.submit_url, args=args) - return context - - def get_initial(self): - volume = self.get_object() - return {'volume_id': self.kwargs["volume_id"], - 'name': volume.name, - 'description': volume.description, - 'bootable': volume.is_bootable} - - -class EditAttachmentsView(tables.DataTableView, forms.ModalFormView): - table_class = project_tables.AttachmentsTable - form_class = project_forms.AttachForm - form_id = "attach_volume_form" - modal_id = "attach_volume_modal" - template_name = 'project/volumes/volumes/attach.html' - submit_url = "horizon:project:volumes:volumes:attach" - success_url = reverse_lazy("horizon:project:volumes:index") - page_title = _("Manage Volume Attachments") - - @memoized.memoized_method - def get_object(self): - volume_id = self.kwargs['volume_id'] - try: - return cinder.volume_get(self.request, volume_id) - except Exception: - self._object = None - exceptions.handle(self.request, - _('Unable to retrieve volume information.')) - - def get_data(self): - attachments = [] - volume = self.get_object() - if volume is not None: - for att in volume.attachments: - att['volume_name'] = getattr(volume, 'name', att['device']) - attachments.append(att) - return attachments - - def get_initial(self): - try: - instances, has_more = api.nova.server_list(self.request) - except Exception: - instances = [] - exceptions.handle(self.request, - _("Unable to retrieve attachment information.")) - return {'volume': self.get_object(), - 'instances': instances} - - @memoized.memoized_method - def get_form(self, **kwargs): - form_class = kwargs.get('form_class', self.get_form_class()) - return super(EditAttachmentsView, self).get_form(form_class) - - def get_context_data(self, **kwargs): - context = super(EditAttachmentsView, self).get_context_data(**kwargs) - context['form'] = self.get_form() - volume = self.get_object() - args = (self.kwargs['volume_id'],) - context['submit_url'] = reverse(self.submit_url, args=args) - if volume and volume.status == 'available': - context['show_attach'] = True - else: - context['show_attach'] = False - context['volume'] = volume - if self.request.is_ajax(): - context['hide'] = True - return context - - def get(self, request, *args, **kwargs): - # Table action handling - handled = self.construct_tables() - if handled: - return handled - return self.render_to_response(self.get_context_data(**kwargs)) - - def post(self, request, *args, **kwargs): - form = self.get_form() - if form.is_valid(): - return self.form_valid(form) - else: - return self.get(request, *args, **kwargs) - - -class RetypeView(forms.ModalFormView): - form_class = project_forms.RetypeForm - modal_id = "retype_volume_modal" - template_name = 'project/volumes/volumes/retype.html' - submit_label = _("Change Volume Type") - submit_url = "horizon:project:volumes:volumes:retype" - success_url = reverse_lazy("horizon:project:volumes:index") - page_title = _("Change Volume Type") - - @memoized.memoized_method - def get_data(self): - try: - volume_id = self.kwargs['volume_id'] - volume = cinder.volume_get(self.request, volume_id) - except Exception: - error_message = _( - 'Unable to retrieve volume information for volume: "%s"') \ - % volume_id - exceptions.handle(self.request, - error_message, - redirect=self.success_url) - - return volume - - def get_context_data(self, **kwargs): - context = super(RetypeView, self).get_context_data(**kwargs) - context['volume'] = self.get_data() - args = (self.kwargs['volume_id'],) - context['submit_url'] = reverse(self.submit_url, args=args) - return context - - def get_initial(self): - volume = self.get_data() - - return {'id': self.kwargs['volume_id'], - 'name': volume.name, - 'volume_type': volume.volume_type} - - -class EncryptionDetailView(generic.TemplateView): - template_name = 'project/volumes/volumes/encryption_detail.html' - page_title = _("Volume Encryption Details: {{ volume.name }}") - - def get_context_data(self, **kwargs): - context = super(EncryptionDetailView, self).get_context_data(**kwargs) - volume = self.get_volume_data() - context["encryption_metadata"] = self.get_encryption_data() - context["volume"] = volume - context["page_title"] = _("Volume Encryption Details: " - "%(volume_name)s") % {'volume_name': - volume.name} - return context - - @memoized.memoized_method - def get_encryption_data(self): - try: - volume_id = self.kwargs['volume_id'] - self._encryption_metadata = \ - cinder.volume_get_encryption_metadata(self.request, - volume_id) - except Exception: - redirect = self.get_redirect_url() - exceptions.handle(self.request, - _('Unable to retrieve volume encryption ' - 'details.'), - redirect=redirect) - return self._encryption_metadata - - @memoized.memoized_method - def get_volume_data(self): - try: - volume_id = self.kwargs['volume_id'] - volume = cinder.volume_get(self.request, volume_id) - except Exception: - redirect = self.get_redirect_url() - exceptions.handle(self.request, - _('Unable to retrieve volume details.'), - redirect=redirect) - return volume - - def get_redirect_url(self): - return reverse('horizon:project:volumes:index') - - -class DownloadTransferCreds(generic.View): - # TODO(Itxaka): Remove cache_control in django >= 1.9 - # https://code.djangoproject.com/ticket/13008 - @method_decorator(cache_control(max_age=0, no_cache=True, - no_store=True, must_revalidate=True)) - @method_decorator(never_cache) - def get(self, request, transfer_id, auth_key): - try: - transfer = cinder.transfer_get(self.request, transfer_id) - except Exception: - transfer = None - response = http.HttpResponse(content_type='application/text') - response['Content-Disposition'] = \ - 'attachment; filename=%s.txt' % slugify(transfer_id) - response.write('%s: %s\n%s: %s\n%s: %s' % ( - _("Transfer name"), - getattr(transfer, 'name', ''), - _("Transfer ID"), - transfer_id, - _("Authorization Key"), - auth_key)) - response['Content-Length'] = str(len(response.content)) - return response diff --git a/openstack_dashboard/enabled/_1040_project_volumes_panel.py b/openstack_dashboard/enabled/_1320_project_volumes_panel.py similarity index 93% rename from openstack_dashboard/enabled/_1040_project_volumes_panel.py rename to openstack_dashboard/enabled/_1320_project_volumes_panel.py index 5f92f42a8c..10dd491d71 100644 --- a/openstack_dashboard/enabled/_1040_project_volumes_panel.py +++ b/openstack_dashboard/enabled/_1320_project_volumes_panel.py @@ -3,7 +3,7 @@ PANEL = 'volumes' # The slug of the dashboard the PANEL associated with. Required. PANEL_DASHBOARD = 'project' # The slug of the panel group the PANEL is associated with. -PANEL_GROUP = 'compute' +PANEL_GROUP = 'volumes' # Python panel class of the PANEL to be added. ADD_PANEL = 'openstack_dashboard.dashboards.project.volumes.panel.Volumes'