From 17b7e91472c530de4ae61abc5b97db0d5ea4b428 Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Sun, 19 Nov 2017 12:49:53 +0000 Subject: [PATCH] Show volume snapshots in admin volume detail page To show volume snapshots from different projects, we need to specify all_tenants=True to the list operation. Also fixes an issue that the actions in the snapshots table in the admin volume detail page refer to the project dashboard. Change-Id: I8dc50625e0e84fd2ce81d731bd54cbd0e08b100d Closes-Bug: #1733186 --- .../dashboards/admin/snapshots/tables.py | 5 ++ .../dashboards/admin/volumes/tabs.py | 5 +- .../dashboards/admin/volumes/tests.py | 50 +++++++++++++++++++ .../dashboards/admin/volumes/views.py | 5 ++ .../dashboards/project/volumes/tabs.py | 22 ++------ .../dashboards/project/volumes/tests.py | 4 +- .../dashboards/project/volumes/views.py | 15 ++++-- 7 files changed, 78 insertions(+), 28 deletions(-) diff --git a/openstack_dashboard/dashboards/admin/snapshots/tables.py b/openstack_dashboard/dashboards/admin/snapshots/tables.py index 51b8fb45ef..215ce88ac7 100644 --- a/openstack_dashboard/dashboards/admin/snapshots/tables.py +++ b/openstack_dashboard/dashboards/admin/snapshots/tables.py @@ -78,3 +78,8 @@ class VolumeSnapshotsTable(volumes_tables.VolumesTableBase): status_columns = ("status",) columns = ('tenant', 'host', 'name', 'description', 'size', 'status', 'volume_name',) + + +class VolumeDetailsSnapshotsTable(VolumeSnapshotsTable): + class Meta(VolumeSnapshotsTable.Meta): + columns = ('name', 'description', 'size', 'status', 'volume_name',) diff --git a/openstack_dashboard/dashboards/admin/volumes/tabs.py b/openstack_dashboard/dashboards/admin/volumes/tabs.py index fb93e5d024..b4bddd1328 100644 --- a/openstack_dashboard/dashboards/admin/volumes/tabs.py +++ b/openstack_dashboard/dashboards/admin/volumes/tabs.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack_dashboard.dashboards.admin.snapshots import tables from openstack_dashboard.dashboards.project.volumes import tabs as project_tabs @@ -27,8 +28,8 @@ class OverviewTab(project_tabs.OverviewTab): class SnapshotTab(project_tabs.SnapshotTab): - pass + table_classes = (tables.VolumeDetailsSnapshotsTable,) class VolumeDetailTabs(project_tabs.VolumeDetailTabs): - tabs = (OverviewTab, project_tabs.SnapshotTab) + tabs = (OverviewTab, SnapshotTab) diff --git a/openstack_dashboard/dashboards/admin/volumes/tests.py b/openstack_dashboard/dashboards/admin/volumes/tests.py index 56913a8712..423f192282 100644 --- a/openstack_dashboard/dashboards/admin/volumes/tests.py +++ b/openstack_dashboard/dashboards/admin/volumes/tests.py @@ -30,6 +30,7 @@ from openstack_dashboard.test import helpers as test from openstack_dashboard.dashboards.admin.snapshots import forms +DETAIL_URL = ('horizon:admin:volumes:detail') INDEX_URL = reverse('horizon:admin:volumes:index') @@ -385,3 +386,52 @@ class VolumeTests(test.BaseAdminViewTests): mock_get.assert_called_once_with(test.IsHttpRequest(), volume.id) self.assertNotContains(res, status_option) + + def test_detail_view_snapshot_tab(self): + volume = self.cinder_volumes.first() + server = self.servers.first() + snapshots = self.cinder_volume_snapshots.list() + this_volume_snapshots = [snapshot for snapshot in snapshots + if snapshot.volume_id == volume.id] + volume.attachments = [{"server_id": server.id}] + volume_limits = self.cinder_limits['absolute'] + with mock.patch.object(api.nova, 'server_get', + return_value=server) as mock_server_get, \ + mock.patch.object( + cinder, 'tenant_absolute_limits', + return_value=volume_limits) as mock_limits, \ + mock.patch.object( + cinder, 'volume_get', + return_value=volume) as mock_volume_get, \ + mock.patch.object( + cinder, 'volume_snapshot_list', + return_value=this_volume_snapshots) \ + as mock_snapshot_list, \ + mock.patch.object( + cinder, 'message_list', + return_value=[]) as mock_message_list: + + url = (reverse(DETAIL_URL, args=[volume.id]) + '?' + + '='.join(['tab', 'volume_details__snapshots_tab'])) + res = self.client.get(url) + + self.assertTemplateUsed(res, 'horizon/common/_detail.html') + self.assertEqual(res.context['volume'].id, volume.id) + self.assertEqual(len(res.context['table'].data), + len(this_volume_snapshots)) + self.assertNoMessages() + + mock_server_get.assert_called_once_with(test.IsHttpRequest(), + server.id) + mock_volume_get.assert_called_once_with(test.IsHttpRequest(), + volume.id) + mock_snapshot_list.assert_called_once_with( + test.IsHttpRequest(), + search_opts={'volume_id': volume.id, 'all_tenants': True}) + mock_limits.assert_called_once() + mock_message_list.assert_called_once_with( + test.IsHttpRequest(), + { + 'resource_uuid': volume.id, + 'resource_type': 'volume' + }) diff --git a/openstack_dashboard/dashboards/admin/volumes/views.py b/openstack_dashboard/dashboards/admin/volumes/views.py index d0798c5a22..7c096a18cf 100644 --- a/openstack_dashboard/dashboards/admin/volumes/views.py +++ b/openstack_dashboard/dashboards/admin/volumes/views.py @@ -118,6 +118,11 @@ class DetailView(volumes_views.DetailView): context["actions"] = table.render_row_actions(context["volume"]) return context + def get_search_opts(self, volume): + search_opts = super(DetailView, self).get_search_opts(volume) + search_opts['all_tenants'] = True + return search_opts + def get_redirect_url(self): return reverse('horizon:admin:volumes:index') diff --git a/openstack_dashboard/dashboards/project/volumes/tabs.py b/openstack_dashboard/dashboards/project/volumes/tabs.py index 46b13b4749..145bec7897 100644 --- a/openstack_dashboard/dashboards/project/volumes/tabs.py +++ b/openstack_dashboard/dashboards/project/volumes/tabs.py @@ -14,10 +14,8 @@ from django.utils.translation import ugettext_lazy as _ -from horizon import exceptions from horizon import tabs -from openstack_dashboard.api import cinder from openstack_dashboard.dashboards.project.snapshots import tables @@ -42,26 +40,12 @@ class SnapshotTab(tabs.TableTab): name = _("Snapshots") slug = "snapshots_tab" template_name = "horizon/common/_detail_table.html" + dashboard = 'project' preload = False def get_volume_snapshots_data(self): - volume_id = self.tab_group.kwargs['volume_id'] - try: - snapshots = cinder.volume_snapshot_list( - self.request, search_opts={'volume_id': volume_id}) - except Exception: - snapshots = [] - exceptions.handle(self.request, - _("Unable to retrieve volume snapshots for " - "volume %s.") % volume_id) - - try: - volume = cinder.volume_get(self.request, volume_id) - except Exception: - volume = None - exceptions.handle(self.request, - _("Unable to retrieve volume details for " - "volume %s.") % volume_id) + snapshots = self.tab_group.kwargs['snapshots'] + volume = self.tab_group.kwargs['volume'] if volume is not None: for snapshot in snapshots: diff --git a/openstack_dashboard/dashboards/project/volumes/tests.py b/openstack_dashboard/dashboards/project/volumes/tests.py index bff5b9895d..27f488b600 100644 --- a/openstack_dashboard/dashboards/project/volumes/tests.py +++ b/openstack_dashboard/dashboards/project/volumes/tests.py @@ -1423,8 +1423,8 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): len(this_volume_snapshots)) self.assertNoMessages() - mock_get.assert_called_with(test.IsHttpRequest(), volume.id) - mock_snapshot_list.assert_called_with( + mock_get.assert_called_once_with(test.IsHttpRequest(), volume.id) + mock_snapshot_list.assert_called_once_with( test.IsHttpRequest(), search_opts={'volume_id': volume.id}) mock_limits.assert_called_once() mock_message.assert_called_once_with( diff --git a/openstack_dashboard/dashboards/project/volumes/views.py b/openstack_dashboard/dashboards/project/volumes/views.py index 209447fb50..1cc93a399e 100644 --- a/openstack_dashboard/dashboards/project/volumes/views.py +++ b/openstack_dashboard/dashboards/project/volumes/views.py @@ -147,7 +147,7 @@ class DetailView(tabs.TabbedTableView): def get_context_data(self, **kwargs): context = super(DetailView, self).get_context_data(**kwargs) - volume = self.get_data() + volume, snapshots = self.get_data() table = volume_tables.VolumesTable(self.request) context["volume"] = volume context["url"] = self.get_redirect_url() @@ -156,13 +156,17 @@ class DetailView(tabs.TabbedTableView): volume.status_label = filters.get_display_label(choices, volume.status) return context + def get_search_opts(self, volume): + return {'volume_id': volume.id} + @memoized.memoized_method def get_data(self): try: volume_id = self.kwargs['volume_id'] volume = cinder.volume_get(self.request, volume_id) + search_opts = self.get_search_opts(volume) snapshots = cinder.volume_snapshot_list( - self.request, search_opts={'volume_id': volume.id}) + self.request, search_opts=search_opts) if snapshots: setattr(volume, 'has_snapshot', True) for att in volume.attachments: @@ -185,14 +189,15 @@ class DetailView(tabs.TabbedTableView): _('Unable to retrieve volume messages.'), ignore=True, ) - return volume + return volume, snapshots 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) + volume, snapshots = self.get_data() + return self.tab_group_class( + request, volume=volume, snapshots=snapshots, **kwargs) class CreateView(forms.ModalFormView):