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:
parent
b12c534d5c
commit
334789312b
@ -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.
|
||||
|
@ -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
|
@ -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',)
|
@ -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,)
|
108
openstack_dashboard/dashboards/admin/volumes/snapshots/tests.py
Normal file
108
openstack_dashboard/dashboards/admin/volumes/snapshots/tests.py
Normal 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)
|
@ -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'),
|
||||
)
|
@ -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')
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
||||
|
||||
|
@ -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 %}
|
||||
|
||||
|
@ -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 %}
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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')),
|
||||
)
|
||||
|
@ -13,7 +13,7 @@
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Admin views for managing volumes.
|
||||
Admin views for managing volumes and snapshots.
|
||||
"""
|
||||
|
||||
from horizon import tabs
|
||||
|
@ -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])
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user