From 334789312bc9e50c391861ddd9e34012dc25129e Mon Sep 17 00:00:00 2001 From: Julie Gravel Date: Thu, 10 Jul 2014 03:34:57 -0700 Subject: [PATCH] Add Volume Snapshots table to Admin Volumes This is part 2 of the work for the BP. The Volume Snapshots table resides inside the new "Volume Snapshots" tab which is the second tab within the Admin Volumes panel. There are two table actions: "Delete Volume Snapshot" and "Update Volume Snapshot Status". The Update Volume Snapshot Status action implements a cinder command that was only available through CLI as per stated in the BP. Change-Id: Ife2da2c142467e47a7ac5bfcb8a477ff578b4d39 Partial-Implements: blueprint cinder-reset-snapshot-state Closes-Bug: #1332077 --- openstack_dashboard/api/cinder.py | 10 +- .../admin/volumes/snapshots/__init__.py | 0 .../admin/volumes/snapshots/forms.py | 49 ++++++++ .../admin/volumes/snapshots/tables.py | 75 ++++++++++++ .../admin/volumes/snapshots/tabs.py | 33 ++++++ .../admin/volumes/snapshots/tests.py | 108 ++++++++++++++++++ .../admin/volumes/snapshots/urls.py | 26 +++++ .../admin/volumes/snapshots/views.py | 63 ++++++++++ .../dashboards/admin/volumes/tabs.py | 51 ++++++++- .../volumes/templates/volumes/index.html | 4 +- .../volumes/snapshots/_detail_overview.html | 38 ++++++ .../volumes/snapshots/_update_status.html | 30 +++++ .../volumes/snapshots/update_status.html | 11 ++ .../volumes/volumes/extras/_create.html | 2 +- .../volumes/volumes/extras/_edit.html | 2 +- .../volumes/volumes/extras/_index.html | 2 +- .../dashboards/admin/volumes/tests.py | 43 ++++++- .../dashboards/admin/volumes/urls.py | 5 + .../dashboards/admin/volumes/views.py | 2 +- .../admin/volumes/volumes/extras/tests.py | 3 +- .../dashboards/admin/volumes/volumes/views.py | 3 + .../project/volumes/snapshots/tabs.py | 5 +- .../project/volumes/snapshots/views.py | 5 +- .../project/volumes/volumes/views.py | 5 +- .../test/api_tests/cinder_tests.py | 12 +- 25 files changed, 568 insertions(+), 19 deletions(-) create mode 100644 openstack_dashboard/dashboards/admin/volumes/snapshots/__init__.py create mode 100644 openstack_dashboard/dashboards/admin/volumes/snapshots/forms.py create mode 100644 openstack_dashboard/dashboards/admin/volumes/snapshots/tables.py create mode 100644 openstack_dashboard/dashboards/admin/volumes/snapshots/tabs.py create mode 100644 openstack_dashboard/dashboards/admin/volumes/snapshots/tests.py create mode 100644 openstack_dashboard/dashboards/admin/volumes/snapshots/urls.py create mode 100644 openstack_dashboard/dashboards/admin/volumes/snapshots/views.py create mode 100644 openstack_dashboard/dashboards/admin/volumes/templates/volumes/snapshots/_detail_overview.html create mode 100644 openstack_dashboard/dashboards/admin/volumes/templates/volumes/snapshots/_update_status.html create mode 100644 openstack_dashboard/dashboards/admin/volumes/templates/volumes/snapshots/update_status.html diff --git a/openstack_dashboard/api/cinder.py b/openstack_dashboard/api/cinder.py index edd5839f87..55bfa745d3 100644 --- a/openstack_dashboard/api/cinder.py +++ b/openstack_dashboard/api/cinder.py @@ -229,11 +229,12 @@ def volume_snapshot_get(request, snapshot_id): return VolumeSnapshot(snapshot) -def volume_snapshot_list(request): +def volume_snapshot_list(request, search_opts=None): c_client = cinderclient(request) if c_client is None: return [] - return [VolumeSnapshot(s) for s in c_client.volume_snapshots.list()] + return [VolumeSnapshot(s) for s in c_client.volume_snapshots.list( + search_opts=search_opts)] def volume_snapshot_create(request, volume_id, name, @@ -259,6 +260,11 @@ def volume_snapshot_update(request, snapshot_id, name, description): **snapshot_data) +def volume_snapshot_reset_state(request, snapshot_id, state): + return cinderclient(request).volume_snapshots.reset_state( + snapshot_id, state) + + @memoized def volume_backup_supported(request): """This method will determine if cinder supports backup. diff --git a/openstack_dashboard/dashboards/admin/volumes/snapshots/__init__.py b/openstack_dashboard/dashboards/admin/volumes/snapshots/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/admin/volumes/snapshots/forms.py b/openstack_dashboard/dashboards/admin/volumes/snapshots/forms.py new file mode 100644 index 0000000000..e244e42261 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/volumes/snapshots/forms.py @@ -0,0 +1,49 @@ +# 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 exceptions +from horizon import forms +from horizon import messages + +from openstack_dashboard.api import cinder + +# This set of states was pulled from cinder's snapshot_actions.py +STATUS_CHOICES = ( + ('available', _('Available')), + ('creating', _('Creating')), + ('deleting', _('Deleting')), + ('error', _('Error')), + ('error_deleting', _('Error_Deleting')), +) + + +class UpdateStatus(forms.SelfHandlingForm): + status = forms.ChoiceField(label=_("Status"), choices=STATUS_CHOICES) + + def handle(self, request, data): + try: + cinder.volume_snapshot_reset_state(request, + self.initial['snapshot_id'], + data['status']) + + choices = dict(STATUS_CHOICES) + choice = choices[data['status']] + messages.success(request, _('Successfully updated volume snapshot' + ' status: "%s".') % choice) + return True + except Exception: + exceptions.handle(request, + _('Unable to update volume snapshot status.')) + return False diff --git a/openstack_dashboard/dashboards/admin/volumes/snapshots/tables.py b/openstack_dashboard/dashboards/admin/volumes/snapshots/tables.py new file mode 100644 index 0000000000..875cbf73ce --- /dev/null +++ b/openstack_dashboard/dashboards/admin/volumes/snapshots/tables.py @@ -0,0 +1,75 @@ +# 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 exceptions +from horizon import tables + +from openstack_dashboard.api import cinder +from openstack_dashboard.api import keystone + +from openstack_dashboard.dashboards.project.volumes.snapshots \ + import tables as snapshots_tables +from openstack_dashboard.dashboards.project.volumes.volumes \ + import tables as volumes_tables + + +class UpdateVolumeSnapshotStatus(tables.LinkAction): + name = "update_status" + verbose_name = _("Update Status") + url = "horizon:admin:volumes:snapshots:update_status" + classes = ("ajax-modal",) + icon = "pencil" + policy_rules = (("volume", + "snapshot_extension:snapshot_actions:update_snapshot_status"),) + + +class UpdateRow(tables.Row): + ajax = True + + def get_data(self, request, snapshot_id): + snapshot = cinder.volume_snapshot_get(request, snapshot_id) + snapshot._volume = cinder.volume_get(request, snapshot.volume_id) + snapshot.host_name = getattr(snapshot._volume, + 'os-vol-host-attr:host') + tenant_id = getattr(snapshot._volume, + 'os-vol-tenant-attr:tenant_id') + try: + tenant = keystone.tenant_get(request, tenant_id) + snapshot.tenant_name = getattr(tenant, "name") + except Exception: + msg = _('Unable to retrieve volume project information.') + exceptions.handle(request, msg) + + return snapshot + + +class VolumeSnapshotsTable(volumes_tables.VolumesTableBase): + name = tables.Column("name", verbose_name=_("Name"), + link="horizon:admin:volumes:snapshots:detail") + volume_name = snapshots_tables.SnapshotVolumeNameColumn( + "name", verbose_name=_("Volume Name"), + link="horizon:admin:volumes:volumes:detail") + host = tables.Column("host_name", verbose_name=_("Host")) + tenant = tables.Column("tenant_name", verbose_name=_("Project")) + + class Meta: + name = "volume_snapshots" + verbose_name = _("Volume Snapshots") + table_actions = (snapshots_tables.DeleteVolumeSnapshot,) + row_actions = (snapshots_tables.DeleteVolumeSnapshot, + UpdateVolumeSnapshotStatus,) + row_class = UpdateRow + status_columns = ("status",) + columns = ('tenant', 'host', 'name', 'description', 'size', 'status', + 'volume_name',) diff --git a/openstack_dashboard/dashboards/admin/volumes/snapshots/tabs.py b/openstack_dashboard/dashboards/admin/volumes/snapshots/tabs.py new file mode 100644 index 0000000000..e229f9ce37 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/volumes/snapshots/tabs.py @@ -0,0 +1,33 @@ +# 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.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import tabs + +from openstack_dashboard.dashboards.project.volumes.snapshots \ + import tabs as overview_tab + + +class OverviewTab(overview_tab.OverviewTab): + name = _("Overview") + slug = "overview" + template_name = ("admin/volumes/snapshots/_detail_overview.html") + + def get_redirect_url(self): + return reverse('horizon:admin:volumes:index') + + +class SnapshotDetailsTabs(tabs.TabGroup): + slug = "snapshot_details" + tabs = (OverviewTab,) diff --git a/openstack_dashboard/dashboards/admin/volumes/snapshots/tests.py b/openstack_dashboard/dashboards/admin/volumes/snapshots/tests.py new file mode 100644 index 0000000000..84a5abc111 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/volumes/snapshots/tests.py @@ -0,0 +1,108 @@ +# 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.core.urlresolvers import reverse +from django import http +from mox import IsA # noqa + +from openstack_dashboard.api import cinder +from openstack_dashboard.test import helpers as test + + +INDEX_URL = reverse('horizon:admin:volumes:index') + + +class VolumeSnapshotsViewTests(test.BaseAdminViewTests): + @test.create_stubs({cinder: ('volume_snapshot_reset_state', + 'volume_snapshot_get')}) + def test_update_snapshot_status(self): + snapshot = self.cinder_volume_snapshots.first() + state = 'error' + + cinder.volume_snapshot_get(IsA(http.HttpRequest), snapshot.id) \ + .AndReturn(snapshot) + cinder.volume_snapshot_reset_state(IsA(http.HttpRequest), + snapshot.id, + state) + self.mox.ReplayAll() + + formData = {'status': state} + url = reverse('horizon:admin:volumes:snapshots:update_status', + args=(snapshot.id,)) + res = self.client.post(url, formData) + self.assertNoFormErrors(res) + + @test.create_stubs({cinder: ('volume_snapshot_get', + 'volume_get')}) + def test_get_volume_snapshot_details(self): + volume = self.cinder_volumes.first() + snapshot = self.cinder_volume_snapshots.first() + + cinder.volume_get(IsA(http.HttpRequest), volume.id). \ + AndReturn(volume) + cinder.volume_snapshot_get(IsA(http.HttpRequest), snapshot.id). \ + AndReturn(snapshot) + + self.mox.ReplayAll() + + url = reverse('horizon:admin:volumes:snapshots:detail', + args=[snapshot.id]) + res = self.client.get(url) + + self.assertContains(res, + "

Volume Snapshot Details: %s

" % + snapshot.name, + 1, 200) + self.assertContains(res, "
test snapshot
", 1, 200) + self.assertContains(res, "
%s
" % snapshot.id, 1, 200) + self.assertContains(res, "
Available
", 1, 200) + + @test.create_stubs({cinder: ('volume_snapshot_get', + 'volume_get')}) + def test_get_volume_snapshot_details_with_snapshot_exception(self): + # Test to verify redirect if get volume snapshot fails + snapshot = self.cinder_volume_snapshots.first() + + cinder.volume_snapshot_get(IsA(http.HttpRequest), snapshot.id).\ + AndRaise(self.exceptions.cinder) + + self.mox.ReplayAll() + + url = reverse('horizon:admin:volumes:snapshots:detail', + args=[snapshot.id]) + res = self.client.get(url) + + self.assertNoFormErrors(res) + self.assertMessageCount(error=1) + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({cinder: ('volume_snapshot_get', + 'volume_get')}) + def test_get_volume_snapshot_details_with_volume_exception(self): + # Test to verify redirect if get volume fails + volume = self.cinder_volumes.first() + snapshot = self.cinder_volume_snapshots.first() + + cinder.volume_get(IsA(http.HttpRequest), volume.id). \ + AndRaise(self.exceptions.cinder) + cinder.volume_snapshot_get(IsA(http.HttpRequest), snapshot.id). \ + AndReturn(snapshot) + + self.mox.ReplayAll() + + url = reverse('horizon:admin:volumes:snapshots:detail', + args=[snapshot.id]) + res = self.client.get(url) + + self.assertNoFormErrors(res) + self.assertMessageCount(error=1) + self.assertRedirectsNoFollow(res, INDEX_URL) diff --git a/openstack_dashboard/dashboards/admin/volumes/snapshots/urls.py b/openstack_dashboard/dashboards/admin/volumes/snapshots/urls.py new file mode 100644 index 0000000000..59d81fc378 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/volumes/snapshots/urls.py @@ -0,0 +1,26 @@ +# 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 patterns # noqa +from django.conf.urls import url # noqa + +from openstack_dashboard.dashboards.admin.volumes.snapshots import views + + +urlpatterns = patterns('', + url(r'^(?P[^/]+)$', + views.DetailView.as_view(), + name='detail'), + url(r'^(?P[^/]+)/update_status/$', + views.UpdateStatusView.as_view(), + name='update_status'), +) diff --git a/openstack_dashboard/dashboards/admin/volumes/snapshots/views.py b/openstack_dashboard/dashboards/admin/volumes/snapshots/views.py new file mode 100644 index 0000000000..a068d9278c --- /dev/null +++ b/openstack_dashboard/dashboards/admin/volumes/snapshots/views.py @@ -0,0 +1,63 @@ +# 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.core.urlresolvers import reverse +from django.core.urlresolvers import reverse_lazy +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon.utils import memoized + +from openstack_dashboard.api import cinder + +from openstack_dashboard.dashboards.admin.volumes.snapshots \ + import forms as vol_snapshot_forms +from openstack_dashboard.dashboards.admin.volumes.snapshots \ + import tabs as vol_snapshot_tabs +from openstack_dashboard.dashboards.project.volumes.snapshots \ + import views + + +class UpdateStatusView(forms.ModalFormView): + form_class = vol_snapshot_forms.UpdateStatus + template_name = 'admin/volumes/snapshots/update_status.html' + success_url = reverse_lazy("horizon:admin:volumes:snapshots_tab") + + @memoized.memoized_method + def get_object(self): + snap_id = self.kwargs['snapshot_id'] + try: + self._object = cinder.volume_snapshot_get(self.request, + snap_id) + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve volume snapshot.'), + redirect=self.success_url) + return self._object + + def get_context_data(self, **kwargs): + context = super(UpdateStatusView, self).get_context_data(**kwargs) + context['snapshot_id'] = self.kwargs["snapshot_id"] + return context + + def get_initial(self): + snapshot = self.get_object() + return {'snapshot_id': self.kwargs["snapshot_id"], + 'status': snapshot.status} + + +class DetailView(views.DetailView): + tab_group_class = vol_snapshot_tabs.SnapshotDetailsTabs + + def get_redirect_url(self): + return reverse('horizon:admin:volumes:index') diff --git a/openstack_dashboard/dashboards/admin/volumes/tabs.py b/openstack_dashboard/dashboards/admin/volumes/tabs.py index 6a029124c8..b8b068e98a 100644 --- a/openstack_dashboard/dashboards/admin/volumes/tabs.py +++ b/openstack_dashboard/dashboards/admin/volumes/tabs.py @@ -15,8 +15,13 @@ from django.utils.translation import ugettext_lazy as _ from horizon import exceptions from horizon import tabs + +from openstack_dashboard import api from openstack_dashboard.api import cinder from openstack_dashboard.api import keystone + +from openstack_dashboard.dashboards.admin.volumes.snapshots \ + import tables as snapshots_tables from openstack_dashboard.dashboards.admin.volumes.volumes \ import tables as volumes_tables from openstack_dashboard.dashboards.project.volumes \ @@ -61,7 +66,51 @@ class VolumeTab(tabs.TableTab, volumes_tabs.VolumeTableMixIn): return volume_types +class SnapshotTab(tabs.TableTab): + table_classes = (snapshots_tables.VolumeSnapshotsTable,) + name = _("Volume Snapshots") + slug = "snapshots_tab" + template_name = ("horizon/common/_detail_table.html") + + def get_volume_snapshots_data(self): + if api.base.is_service_enabled(self.request, 'volume'): + try: + snapshots = cinder.volume_snapshot_list(self.request, + search_opts={'all_tenants': True}) + volumes = cinder.volume_list(self.request, + search_opts={'all_tenants': True}) + volumes = dict((v.id, v) for v in volumes) + except Exception: + snapshots = [] + volumes = {} + exceptions.handle(self.request, _("Unable to retrieve " + "volume snapshots.")) + + # Gather our tenants to correlate against volume IDs + try: + tenants, has_more = keystone.tenant_list(self.request) + except Exception: + tenants = [] + msg = _('Unable to retrieve volume project information.') + exceptions.handle(self.request, msg) + + tenant_dict = dict([(t.id, t) for t in tenants]) + for snapshot in snapshots: + volume = volumes.get(snapshot.volume_id) + tenant_id = getattr(volume, + 'os-vol-tenant-attr:tenant_id', None) + tenant = tenant_dict.get(tenant_id, None) + snapshot._volume = volume + snapshot.tenant_name = getattr(tenant, "name", None) + snapshot.host_name = getattr( + volume, 'os-vol-host-attr:host', None) + + else: + snapshots = [] + return sorted(snapshots, key=lambda snapshot: snapshot.tenant_name) + + class VolumesGroupTabs(tabs.TabGroup): slug = "volumes_group_tabs" - tabs = (VolumeTab,) + tabs = (VolumeTab, SnapshotTab,) sticky = True diff --git a/openstack_dashboard/dashboards/admin/volumes/templates/volumes/index.html b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/index.html index 6725170e20..e4b2d4f30d 100644 --- a/openstack_dashboard/dashboards/admin/volumes/templates/volumes/index.html +++ b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/index.html @@ -7,8 +7,8 @@ {% endblock page_header %} {% block main %} -
-
+
+
{{ tab_group.render }}
diff --git a/openstack_dashboard/dashboards/admin/volumes/templates/volumes/snapshots/_detail_overview.html b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/snapshots/_detail_overview.html new file mode 100644 index 0000000000..16a7203a50 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/snapshots/_detail_overview.html @@ -0,0 +1,38 @@ +{% load i18n sizeformat parse_date %} +{% load url from future %} + +

{% trans "Volume Snapshot Overview" %}

+ +
+

{% trans "Info" %}

+
+
+
{% trans "Name" %}
+
{{ snapshot.name }}
+
{% trans "ID" %}
+
{{ snapshot.id }}
+ {% if snapshot.description %} +
{% trans "Description" %}
+
{{ snapshot.description }}
+ {% endif %} +
{% trans "Status" %}
+
{{ snapshot.status|capfirst }}
+
{% trans "Volume" %}
+
+ + {{ volume.name }} + +
+
+
+ +
+

{% trans "Specs" %}

+
+
+
{% trans "Size" %}
+
{{ snapshot.size }} {% trans "GB" %}
+
{% trans "Created" %}
+
{{ snapshot.created_at|parse_date }}
+
+
diff --git a/openstack_dashboard/dashboards/admin/volumes/templates/volumes/snapshots/_update_status.html b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/snapshots/_update_status.html new file mode 100644 index 0000000000..c1554fcc10 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/snapshots/_update_status.html @@ -0,0 +1,30 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% load url from future %} + +{% block form_id %}{% endblock %} +{% block form_action %}{% url 'horizon:admin:volumes:snapshots:update_status' snapshot_id %}{% endblock %} + +{% block modal_id %}update_volume_snapshot_status_modal{% endblock %} +{% block modal-header %}{% trans "Update Volume Snapshot Status" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description:" %}

+

{% blocktrans %} + The status of a volume snapshot is normally managed automatically. In some circumstances + an administrator may need to explicitly update the status value. This is equivalent to + the cinder snapshot-reset-state command. + {% endblocktrans %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} \ No newline at end of file diff --git a/openstack_dashboard/dashboards/admin/volumes/templates/volumes/snapshots/update_status.html b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/snapshots/update_status.html new file mode 100644 index 0000000000..7397cb59ca --- /dev/null +++ b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/snapshots/update_status.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update Volume Snapshot Status" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Update Volume Snapshot Status") %} +{% endblock page_header %} + +{% block main %} + {% include 'admin/volumes/snapshots/_update_status.html' %} +{% endblock %} \ No newline at end of file diff --git a/openstack_dashboard/dashboards/admin/volumes/templates/volumes/volumes/extras/_create.html b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/volumes/extras/_create.html index 0101b2f719..a2aca44d3e 100644 --- a/openstack_dashboard/dashboards/admin/volumes/templates/volumes/volumes/extras/_create.html +++ b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/volumes/extras/_create.html @@ -23,6 +23,6 @@ {% block modal-footer %} - {% trans "Cancel" %} + {% trans "Cancel" %} {% endblock %} diff --git a/openstack_dashboard/dashboards/admin/volumes/templates/volumes/volumes/extras/_edit.html b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/volumes/extras/_edit.html index 93ecac14bd..50af81d4a3 100644 --- a/openstack_dashboard/dashboards/admin/volumes/templates/volumes/volumes/extras/_edit.html +++ b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/volumes/extras/_edit.html @@ -23,6 +23,6 @@ {% block modal-footer %} - {% trans "Cancel" %} + {% trans "Cancel" %} {% endblock %} diff --git a/openstack_dashboard/dashboards/admin/volumes/templates/volumes/volumes/extras/_index.html b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/volumes/extras/_index.html index ae126921d6..b79f34a7fb 100644 --- a/openstack_dashboard/dashboards/admin/volumes/templates/volumes/volumes/extras/_index.html +++ b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/volumes/extras/_index.html @@ -10,6 +10,6 @@ {% endblock %} {% block modal-footer %} - {% trans "Close" %} + {% trans "Close" %} {% endblock %} diff --git a/openstack_dashboard/dashboards/admin/volumes/tests.py b/openstack_dashboard/dashboards/admin/volumes/tests.py index 164720838f..a89792a1a6 100644 --- a/openstack_dashboard/dashboards/admin/volumes/tests.py +++ b/openstack_dashboard/dashboards/admin/volumes/tests.py @@ -25,7 +25,8 @@ from openstack_dashboard.test import helpers as test class VolumeTests(test.BaseAdminViewTests): @test.create_stubs({api.nova: ('server_list',), cinder: ('volume_list', - 'volume_type_list',), + 'volume_type_list', + 'volume_snapshot_list'), keystone: ('tenant_list',)}) def test_index(self): cinder.volume_list(IsA(http.HttpRequest), search_opts={ @@ -38,6 +39,15 @@ class VolumeTests(test.BaseAdminViewTests): keystone.tenant_list(IsA(http.HttpRequest)) \ .AndReturn([self.tenants.list(), False]) + cinder.volume_snapshot_list(IsA(http.HttpRequest), search_opts={ + 'all_tenants': True}).\ + AndReturn(self.cinder_volume_snapshots.list()) + cinder.volume_list(IsA(http.HttpRequest), search_opts={ + 'all_tenants': True}).\ + AndReturn(self.cinder_volumes.list()) + keystone.tenant_list(IsA(http.HttpRequest)). \ + AndReturn([self.tenants.list(), False]) + self.mox.ReplayAll() res = self.client.get(reverse('horizon:admin:volumes:index')) @@ -110,3 +120,34 @@ class VolumeTests(test.BaseAdminViewTests): args=(volume.id,)), formData) self.assertNoFormErrors(res) + + @test.create_stubs({api.nova: ('server_list',), + cinder: ('volume_list', + 'volume_type_list', + 'volume_snapshot_list',), + keystone: ('tenant_list',)}) + def test_snapshot_tab(self): + cinder.volume_list(IsA(http.HttpRequest), search_opts={ + 'all_tenants': True}).\ + AndReturn(self.cinder_volumes.list()) + api.nova.server_list(IsA(http.HttpRequest), search_opts={ + 'all_tenants': True}).\ + AndReturn([self.servers.list(), False]) + cinder.volume_type_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.list()) + keystone.tenant_list(IsA(http.HttpRequest)). \ + AndReturn([self.tenants.list(), False]) + + cinder.volume_snapshot_list(IsA(http.HttpRequest), search_opts={ + 'all_tenants': True}). \ + AndReturn(self.cinder_volume_snapshots.list()) + cinder.volume_list(IsA(http.HttpRequest), search_opts={ + 'all_tenants': True}).\ + AndReturn(self.cinder_volumes.list()) + keystone.tenant_list(IsA(http.HttpRequest)). \ + AndReturn([self.tenants.list(), False]) + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:admin:volumes:snapshots_tab')) + self.assertEqual(res.status_code, 200) + self.assertTemplateUsed(res, 'horizon/common/_detail_table.html') diff --git a/openstack_dashboard/dashboards/admin/volumes/urls.py b/openstack_dashboard/dashboards/admin/volumes/urls.py index b91093f168..f8a331230a 100644 --- a/openstack_dashboard/dashboards/admin/volumes/urls.py +++ b/openstack_dashboard/dashboards/admin/volumes/urls.py @@ -14,13 +14,18 @@ from django.conf.urls import include # noqa from django.conf.urls import patterns # noqa from django.conf.urls import url # noqa +from openstack_dashboard.dashboards.admin.volumes.snapshots \ + import urls as snapshot_urls from openstack_dashboard.dashboards.admin.volumes import views from openstack_dashboard.dashboards.admin.volumes.volumes \ import urls as volumes_urls urlpatterns = patterns('', url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^\?tab=volumes_group_tabs__snapshots_tab$', + views.IndexView.as_view(), name='snapshots_tab'), url(r'^\?tab=volumes_group_tabs__volumes_tab$', views.IndexView.as_view(), name='volumes_tab'), url(r'', include(volumes_urls, namespace='volumes')), + url(r'snapshots/', include(snapshot_urls, namespace='snapshots')), ) diff --git a/openstack_dashboard/dashboards/admin/volumes/views.py b/openstack_dashboard/dashboards/admin/volumes/views.py index b30e279213..23b5e79941 100644 --- a/openstack_dashboard/dashboards/admin/volumes/views.py +++ b/openstack_dashboard/dashboards/admin/volumes/views.py @@ -13,7 +13,7 @@ # under the License. """ -Admin views for managing volumes. +Admin views for managing volumes and snapshots. """ from horizon import tabs diff --git a/openstack_dashboard/dashboards/admin/volumes/volumes/extras/tests.py b/openstack_dashboard/dashboards/admin/volumes/volumes/extras/tests.py index a7efc7ac04..15bbe30a88 100644 --- a/openstack_dashboard/dashboards/admin/volumes/volumes/extras/tests.py +++ b/openstack_dashboard/dashboards/admin/volumes/volumes/extras/tests.py @@ -96,10 +96,9 @@ class VolTypeExtrasTests(test.BaseAdminViewTests): 'volume_type_extra_set',), }) def test_extra_edit(self): vol_type = self.cinder_volume_types.first() - extras = [api.cinder.VolTypeExtraSpec(vol_type.id, 'k1', 'v1')] key = 'foo' edit_url = reverse('horizon:admin:volumes:volumes:extras:edit', - args=[vol_type.id, key]) + args=[vol_type.id, key]) index_url = reverse('horizon:admin:volumes:volumes:extras:index', args=[vol_type.id]) diff --git a/openstack_dashboard/dashboards/admin/volumes/volumes/views.py b/openstack_dashboard/dashboards/admin/volumes/volumes/views.py index 6386c40224..be19286bb5 100644 --- a/openstack_dashboard/dashboards/admin/volumes/volumes/views.py +++ b/openstack_dashboard/dashboards/admin/volumes/volumes/views.py @@ -28,6 +28,9 @@ from openstack_dashboard.dashboards.project.volumes.volumes \ class DetailView(volumes_views.DetailView): template_name = "admin/volumes/volumes/detail.html" + def get_redirect_url(self): + return reverse('horizon:admin:volumes:index') + class CreateVolumeTypeView(forms.ModalFormView): form_class = volumes_forms.CreateVolumeType diff --git a/openstack_dashboard/dashboards/project/volumes/snapshots/tabs.py b/openstack_dashboard/dashboards/project/volumes/snapshots/tabs.py index 8dade12b7c..c3e0a018b7 100644 --- a/openstack_dashboard/dashboards/project/volumes/snapshots/tabs.py +++ b/openstack_dashboard/dashboards/project/volumes/snapshots/tabs.py @@ -31,13 +31,16 @@ class OverviewTab(tabs.Tab): snapshot = self.tab_group.kwargs['snapshot'] volume = cinder.volume_get(request, snapshot.volume_id) except Exception: - redirect = reverse('horizon:project:volumes:index') + redirect = self.get_redirect_url() exceptions.handle(self.request, _('Unable to retrieve snapshot details.'), redirect=redirect) return {"snapshot": snapshot, "volume": volume} + def get_redirect_url(self): + return reverse('horizon:project:volumes:index') + class SnapshotDetailTabs(tabs.TabGroup): slug = "snapshot_details" diff --git a/openstack_dashboard/dashboards/project/volumes/snapshots/views.py b/openstack_dashboard/dashboards/project/volumes/snapshots/views.py index 9bafa9a17a..4b50660adc 100644 --- a/openstack_dashboard/dashboards/project/volumes/snapshots/views.py +++ b/openstack_dashboard/dashboards/project/volumes/snapshots/views.py @@ -72,12 +72,15 @@ class DetailView(tabs.TabView): snapshot = api.cinder.volume_snapshot_get(self.request, snapshot_id) except Exception: - redirect = reverse('horizon:project:volumes:index') + redirect = self.get_redirect_url() exceptions.handle(self.request, _('Unable to retrieve snapshot details.'), redirect=redirect) return snapshot + def get_redirect_url(self): + return reverse('horizon:project:volumes:index') + def get_tabs(self, request, *args, **kwargs): snapshot = self.get_data() return self.tab_group_class(request, snapshot=snapshot, **kwargs) diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/views.py b/openstack_dashboard/dashboards/project/volumes/volumes/views.py index 21c2d7ae86..b1ef144e80 100644 --- a/openstack_dashboard/dashboards/project/volumes/volumes/views.py +++ b/openstack_dashboard/dashboards/project/volumes/volumes/views.py @@ -57,12 +57,15 @@ class DetailView(tabs.TabView): att['instance'] = api.nova.server_get(self.request, att['server_id']) except Exception: - redirect = reverse('horizon:project:volumes:index') + 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) diff --git a/openstack_dashboard/test/api_tests/cinder_tests.py b/openstack_dashboard/test/api_tests/cinder_tests.py index 9072ac1740..0392e17533 100644 --- a/openstack_dashboard/test/api_tests/cinder_tests.py +++ b/openstack_dashboard/test/api_tests/cinder_tests.py @@ -33,13 +33,15 @@ class CinderApiTests(test.APITestCase): api.cinder.volume_list(self.request, search_opts=search_opts) def test_volume_snapshot_list(self): + search_opts = {'all_tenants': 1} volume_snapshots = self.cinder_volume_snapshots.list() cinderclient = self.stub_cinderclient() cinderclient.volume_snapshots = self.mox.CreateMockAnything() - cinderclient.volume_snapshots.list().AndReturn(volume_snapshots) + cinderclient.volume_snapshots.list(search_opts=search_opts).\ + AndReturn(volume_snapshots) self.mox.ReplayAll() - api.cinder.volume_snapshot_list(self.request) + api.cinder.volume_snapshot_list(self.request, search_opts=search_opts) def test_volume_snapshot_list_no_volume_configured(self): # remove volume from service catalog @@ -47,14 +49,16 @@ class CinderApiTests(test.APITestCase): for service in catalog: if service["type"] == "volume": self.service_catalog.remove(service) + search_opts = {'all_tenants': 1} volume_snapshots = self.cinder_volume_snapshots.list() cinderclient = self.stub_cinderclient() cinderclient.volume_snapshots = self.mox.CreateMockAnything() - cinderclient.volume_snapshots.list().AndReturn(volume_snapshots) + cinderclient.volume_snapshots.list(search_opts=search_opts).\ + AndReturn(volume_snapshots) self.mox.ReplayAll() - api.cinder.volume_snapshot_list(self.request) + api.cinder.volume_snapshot_list(self.request, search_opts=search_opts) class CinderApiVersionTests(test.TestCase):