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'