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
This commit is contained in:
Julie Gravel 2014-07-10 03:34:57 -07:00
parent b12c534d5c
commit 334789312b
25 changed files with 568 additions and 19 deletions

View File

@ -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.

View File

@ -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

View File

@ -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',)

View File

@ -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,)

View File

@ -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,
"<h2>Volume Snapshot Details: %s</h2>" %
snapshot.name,
1, 200)
self.assertContains(res, "<dd>test snapshot</dd>", 1, 200)
self.assertContains(res, "<dd>%s</dd>" % snapshot.id, 1, 200)
self.assertContains(res, "<dd>Available</dd>", 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)

View File

@ -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<snapshot_id>[^/]+)$',
views.DetailView.as_view(),
name='detail'),
url(r'^(?P<snapshot_id>[^/]+)/update_status/$',
views.UpdateStatusView.as_view(),
name='update_status'),
)

View File

@ -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')

View File

@ -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

View File

@ -7,8 +7,8 @@
{% endblock page_header %}
{% block main %}
<div class="row-fluid">
<div class="span12">
<div class="row">
<div class="col-sm-12">
{{ tab_group.render }}
</div>
</div>

View File

@ -0,0 +1,38 @@
{% load i18n sizeformat parse_date %}
{% load url from future %}
<h3>{% trans "Volume Snapshot Overview" %}</h3>
<div class="info row detail">
<h4>{% trans "Info" %}</h4>
<hr class="header_rule">
<dl>
<dt>{% trans "Name" %}</dt>
<dd>{{ snapshot.name }}</dd>
<dt>{% trans "ID" %}</dt>
<dd>{{ snapshot.id }}</dd>
{% if snapshot.description %}
<dt>{% trans "Description" %}</dt>
<dd>{{ snapshot.description }}</dd>
{% endif %}
<dt>{% trans "Status" %}</dt>
<dd>{{ snapshot.status|capfirst }}</dd>
<dt>{% trans "Volume" %}</dt>
<dd>
<a href="{% url 'horizon:admin:volumes:volumes:detail' snapshot.volume_id %}">
{{ volume.name }}
</a>
</dd>
</dl>
</div>
<div class="specs row detail">
<h4>{% trans "Specs" %}</h4>
<hr class="header_rule">
<dl>
<dt>{% trans "Size" %}</dt>
<dd>{{ snapshot.size }} {% trans "GB" %}</dd>
<dt>{% trans "Created" %}</dt>
<dd>{{ snapshot.created_at|parse_date }}</dd>
</dl>
</div>

View File

@ -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 %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description:" %}</h3>
<p>{% 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 <tt>cinder snapshot-reset-state</tt> command.
{% endblocktrans %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Update Status" %}" />
<a href="{% url 'horizon:admin:volumes:index' %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -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 %}

View File

@ -23,6 +23,6 @@
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create" %}" />
<a href="{% url 'horizon:admin:volumes:volumes:extras:index' vol_type.id %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
<a href="{% url 'horizon:admin:volumes:volumes:extras:index' vol_type.id %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -23,6 +23,6 @@
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Save" %}" />
<a href="{% url 'horizon:admin:volumes:volumes:extras:index' vol_type.id %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
<a href="{% url 'horizon:admin:volumes:volumes:extras:index' vol_type.id %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -10,6 +10,6 @@
{% endblock %}
{% block modal-footer %}
<a href="{% url 'horizon:admin:volumes:index' %}" class="btn secondary cancel close">{% trans "Close" %}</a>
<a href="{% url 'horizon:admin:volumes:index' %}" class="btn btn-default secondary cancel close">{% trans "Close" %}</a>
{% endblock %}

View File

@ -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')

View File

@ -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')),
)

View File

@ -13,7 +13,7 @@
# under the License.
"""
Admin views for managing volumes.
Admin views for managing volumes and snapshots.
"""
from horizon import tabs

View File

@ -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])

View File

@ -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

View File

@ -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"

View File

@ -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)

View File

@ -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)

View File

@ -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):