Move Volume snapshots out of tabbed panel

In doing this I found that the permission check on the
project Volume Snapshots table was incorrectly defined,
resulting in always-denied. Removing a level of nesting
in the permission structure fixed it.

Change-Id: I1bc5bd4820700d29861ec1dcd6c87a41b5230266
Implements: blueprint reorganise-volumes
This commit is contained in:
Richard Jones 2017-02-09 12:57:36 +11:00
parent 5679634fed
commit 97589ec0fb
24 changed files with 235 additions and 213 deletions

View File

@ -31,3 +31,4 @@ from horizon.tables.views import DataTableView # noqa
from horizon.tables.views import MixedDataTableView # noqa from horizon.tables.views import MixedDataTableView # noqa
from horizon.tables.views import MultiTableMixin # noqa from horizon.tables.views import MultiTableMixin # noqa
from horizon.tables.views import MultiTableView # noqa from horizon.tables.views import MultiTableView # noqa
from horizon.tables.views import PagedTableMixin # noqa

View File

@ -357,3 +357,30 @@ class MixedDataTableView(DataTableView):
'in table %s to use MixedDataTableView.' 'in table %s to use MixedDataTableView.'
% self.table._meta.name) % self.table._meta.name)
return self.table return self.table
class PagedTableMixin(object):
def __init__(self, *args, **kwargs):
super(PagedTableMixin, self).__init__(*args, **kwargs)
self._has_prev_data = False
self._has_more_data = False
def has_prev_data(self, table):
return self._has_prev_data
def has_more_data(self, table):
return self._has_more_data
def _get_marker(self):
try:
meta = self.table_class._meta
except AttributeError:
meta = self.table_classes[0]._meta
prev_marker = self.request.GET.get(meta.prev_pagination_param, None)
if prev_marker:
return prev_marker, "asc"
else:
marker = self.request.GET.get(meta.pagination_param, None)
if marker:
return marker, "desc"
return None, "desc"

View File

@ -18,7 +18,7 @@ from horizon import tables
from openstack_dashboard.api import cinder from openstack_dashboard.api import cinder
from openstack_dashboard.api import keystone from openstack_dashboard.api import keystone
from openstack_dashboard.dashboards.project.volumes.snapshots \ from openstack_dashboard.dashboards.project.snapshots \
import tables as snapshots_tables import tables as snapshots_tables
from openstack_dashboard.dashboards.project.volumes.volumes \ from openstack_dashboard.dashboards.project.volumes.volumes \
import tables as volumes_tables import tables as volumes_tables

View File

@ -15,14 +15,14 @@ from django.utils.translation import ugettext_lazy as _
from horizon import tabs from horizon import tabs
from openstack_dashboard.dashboards.project.volumes.snapshots \ from openstack_dashboard.dashboards.project.snapshots \
import tabs as overview_tab import tabs as overview_tab
class OverviewTab(overview_tab.OverviewTab): class OverviewTab(overview_tab.OverviewTab):
name = _("Overview") name = _("Overview")
slug = "overview" slug = "overview"
template_name = ("project/volumes/snapshots/_detail_overview.html") template_name = ("project/snapshots/_detail_overview.html")
def get_redirect_url(self): def get_redirect_url(self):
return reverse('horizon:admin:volumes:index') return reverse('horizon:admin:volumes:index')

View File

@ -24,7 +24,7 @@ from openstack_dashboard.dashboards.admin.volumes.snapshots \
import forms as vol_snapshot_forms import forms as vol_snapshot_forms
from openstack_dashboard.dashboards.admin.volumes.snapshots \ from openstack_dashboard.dashboards.admin.volumes.snapshots \
import tabs as vol_snapshot_tabs import tabs as vol_snapshot_tabs
from openstack_dashboard.dashboards.project.volumes.snapshots \ from openstack_dashboard.dashboards.project.snapshots \
import views import views

View File

@ -33,7 +33,7 @@ from openstack_dashboard.dashboards.project.volumes \
import tabs as volumes_tabs import tabs as volumes_tabs
class VolumeTab(volumes_tabs.PagedTableMixin, tabs.TableTab, class VolumeTab(tables.PagedTableMixin, tabs.TableTab,
volumes_tabs.VolumeTableMixIn, tables.DataTableView): volumes_tabs.VolumeTableMixIn, tables.DataTableView):
table_classes = (volumes_tables.VolumesTable,) table_classes = (volumes_tables.VolumesTable,)
name = _("Volumes") name = _("Volumes")
@ -159,7 +159,7 @@ class VolumeTypesTab(tabs.TableTab, volumes_tabs.VolumeTableMixIn):
return qos_specs return qos_specs
class SnapshotTab(volumes_tabs.PagedTableMixin, tabs.TableTab): class SnapshotTab(tables.PagedTableMixin, tabs.TableTab):
table_classes = (snapshots_tables.VolumeSnapshotsTable,) table_classes = (snapshots_tables.VolumeSnapshotsTable,)
name = _("Volume Snapshots") name = _("Volume Snapshots")
slug = "snapshots_tab" slug = "snapshots_tab"

View File

@ -24,7 +24,7 @@ from mox3.mox import IsA # noqa
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.api import cinder from openstack_dashboard.api import cinder
from openstack_dashboard.api import keystone from openstack_dashboard.api import keystone
from openstack_dashboard.dashboards.project.volumes.snapshots \ from openstack_dashboard.dashboards.project.snapshots \
import tables as snapshot_tables import tables as snapshot_tables
from openstack_dashboard.dashboards.project.volumes.volumes \ from openstack_dashboard.dashboards.project.volumes.volumes \
import tables as volume_tables import tables as volume_tables

View File

@ -40,7 +40,7 @@ class UpdateForm(forms.SelfHandlingForm):
messages.info(request, message) messages.info(request, message)
return True return True
except Exception: except Exception:
redirect = reverse("horizon:project:volumes:index") redirect = reverse("horizon:project:snapshots:index")
exceptions.handle(request, exceptions.handle(request,
_('Unable to update volume snapshot.'), _('Unable to update volume snapshot.'),
redirect=redirect) redirect=redirect)

View File

@ -0,0 +1,26 @@
# Copyright 2017 Rackspace, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
import horizon
class Snapshots(horizon.Panel):
name = _("Snapshots")
slug = 'snapshots'
permissions = (
('openstack.services.volume', 'openstack.services.volumev2'),
)
policy_rules = (("volume", "volume:get_all_snapshots"),)

View File

@ -52,7 +52,7 @@ class LaunchSnapshot(volume_tables.LaunchVolume):
class LaunchSnapshotNG(LaunchSnapshot): class LaunchSnapshotNG(LaunchSnapshot):
name = "launch_snapshot_ng" name = "launch_snapshot_ng"
verbose_name = _("Launch as Instance") verbose_name = _("Launch as Instance")
url = "horizon:project:volumes:snapshots_tab" url = "horizon:project:snapshots:index"
classes = ("btn-launch", ) classes = ("btn-launch", )
ajax = False ajax = False
@ -102,7 +102,7 @@ class DeleteVolumeSnapshot(policy.PolicyTargetMixin, tables.DeleteAction):
class EditVolumeSnapshot(policy.PolicyTargetMixin, tables.LinkAction): class EditVolumeSnapshot(policy.PolicyTargetMixin, tables.LinkAction):
name = "edit" name = "edit"
verbose_name = _("Edit Snapshot") verbose_name = _("Edit Snapshot")
url = "horizon:project:volumes:snapshots:update" url = "horizon:project:snapshots:update"
classes = ("ajax-modal",) classes = ("ajax-modal",)
icon = "pencil" icon = "pencil"
policy_rules = (("volume", "volume:update_snapshot"),) policy_rules = (("volume", "volume:update_snapshot"),)
@ -189,7 +189,7 @@ class VolumeSnapshotsTable(volume_tables.VolumesTableBase):
name = tables.WrappingColumn( name = tables.WrappingColumn(
"name", "name",
verbose_name=_("Name"), verbose_name=_("Name"),
link="horizon:project:volumes:snapshots:detail") link="horizon:project:snapshots:detail")
volume_name = SnapshotVolumeNameColumn( volume_name = SnapshotVolumeNameColumn(
"name", "name",
verbose_name=_("Volume Name"), verbose_name=_("Volume Name"),
@ -213,6 +213,6 @@ class VolumeSnapshotsTable(volume_tables.VolumesTableBase):
UpdateMetadata)) UpdateMetadata))
row_class = UpdateRow row_class = UpdateRow
status_columns = ("status",) status_columns = ("status",)
permissions = [( permissions = [
('openstack.services.volume', 'openstack.services.volumev2'), ('openstack.services.volume', 'openstack.services.volumev2'),
)] ]

View File

@ -24,7 +24,7 @@ from openstack_dashboard.api import cinder
class OverviewTab(tabs.Tab): class OverviewTab(tabs.Tab):
name = _("Overview") name = _("Overview")
slug = "overview" slug = "overview"
template_name = ("project/volumes/snapshots/_detail_overview.html") template_name = ("project/snapshots/_detail_overview.html")
def get_context_data(self, request): def get_context_data(self, request):
try: try:
@ -39,7 +39,7 @@ class OverviewTab(tabs.Tab):
"volume": volume} "volume": volume}
def get_redirect_url(self): def get_redirect_url(self):
return reverse('horizon:project:volumes:index') return reverse('horizon:project:snapshots:index')
class SnapshotDetailTabs(tabs.TabGroup): class SnapshotDetailTabs(tabs.TabGroup):

View File

@ -16,21 +16,112 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django import http from django import http
from django.test.utils import override_settings
from django.utils.http import urlunquote
from mox3.mox import IsA # noqa from mox3.mox import IsA # noqa
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.api import cinder from openstack_dashboard.api import cinder
from openstack_dashboard.dashboards.project.snapshots \
import tables as snapshot_tables
from openstack_dashboard.test import helpers as test from openstack_dashboard.test import helpers as test
from openstack_dashboard.usage import quotas from openstack_dashboard.usage import quotas
INDEX_URL = reverse('horizon:project:volumes:index') INDEX_URL = reverse('horizon:project:snapshots:index')
VOLUME_SNAPSHOTS_TAB_URL = reverse('horizon:project:volumes:snapshots_tab')
class VolumeSnapshotsViewTests(test.TestCase): class VolumeSnapshotsViewTests(test.TestCase):
@test.create_stubs({api.cinder: ('tenant_absolute_limits',
'volume_snapshot_list_paged',
'volume_list',),
api.base: ('is_service_enabled',)})
def _test_snapshots_index_paginated(self, marker, sort_dir, snapshots, url,
has_more, has_prev):
api.base.is_service_enabled(IsA(http.HttpRequest), 'volumev2') \
.AndReturn(True)
api.base.is_service_enabled(IsA(http.HttpRequest), 'volume') \
.AndReturn(True)
api.cinder.volume_snapshot_list_paged(
IsA(http.HttpRequest), marker=marker, sort_dir=sort_dir,
paginate=True).AndReturn([snapshots, has_more, has_prev])
api.cinder.volume_list(IsA(http.HttpRequest)).AndReturn(
self.cinder_volumes.list())
self.mox.ReplayAll()
res = self.client.get(urlunquote(url))
self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(res, 'horizon/common/_data_table_view.html')
self.mox.UnsetStubs()
return res
@override_settings(API_RESULT_PAGE_SIZE=1)
def test_snapshots_index_paginated(self):
mox_snapshots = self.cinder_volume_snapshots.list()
size = settings.API_RESULT_PAGE_SIZE
base_url = INDEX_URL
next = snapshot_tables.VolumeSnapshotsTable._meta.pagination_param
# get first page
expected_snapshots = mox_snapshots[:size]
res = self._test_snapshots_index_paginated(
marker=None, sort_dir="desc", snapshots=expected_snapshots,
url=base_url, has_more=True, has_prev=False)
snapshots = res.context['volume_snapshots_table'].data
self.assertItemsEqual(snapshots, expected_snapshots)
# get second page
expected_snapshots = mox_snapshots[size:2 * size]
marker = expected_snapshots[0].id
url = base_url + "?%s=%s" % (next, marker)
res = self._test_snapshots_index_paginated(
marker=marker, sort_dir="desc", snapshots=expected_snapshots,
url=url, has_more=True, has_prev=True)
snapshots = res.context['volume_snapshots_table'].data
self.assertItemsEqual(snapshots, expected_snapshots)
# get last page
expected_snapshots = mox_snapshots[-size:]
marker = expected_snapshots[0].id
url = base_url + "?%s=%s" % (next, marker)
res = self._test_snapshots_index_paginated(
marker=marker, sort_dir="desc", snapshots=expected_snapshots,
url=url, has_more=False, has_prev=True)
snapshots = res.context['volume_snapshots_table'].data
self.assertItemsEqual(snapshots, expected_snapshots)
@override_settings(API_RESULT_PAGE_SIZE=1)
def test_snapshots_index_paginated_prev_page(self):
mox_snapshots = self.cinder_volume_snapshots.list()
size = settings.API_RESULT_PAGE_SIZE
base_url = INDEX_URL
prev = snapshot_tables.VolumeSnapshotsTable._meta.prev_pagination_param
# prev from some page
expected_snapshots = mox_snapshots[size:2 * size]
marker = expected_snapshots[0].id
url = base_url + "?%s=%s" % (prev, marker)
res = self._test_snapshots_index_paginated(
marker=marker, sort_dir="asc", snapshots=expected_snapshots,
url=url, has_more=True, has_prev=True)
snapshots = res.context['volume_snapshots_table'].data
self.assertItemsEqual(snapshots, expected_snapshots)
# back to first page
expected_snapshots = mox_snapshots[:size]
marker = expected_snapshots[0].id
url = base_url + "?%s=%s" % (prev, marker)
res = self._test_snapshots_index_paginated(
marker=marker, sort_dir="asc", snapshots=expected_snapshots,
url=url, has_more=True, has_prev=False)
snapshots = res.context['volume_snapshots_table'].data
self.assertItemsEqual(snapshots, expected_snapshots)
@test.create_stubs({cinder: ('volume_get',), @test.create_stubs({cinder: ('volume_get',),
quotas: ('tenant_limit_usages',)}) quotas: ('tenant_limit_usages',)})
def test_create_snapshot_get(self): def test_create_snapshot_get(self):
@ -77,7 +168,7 @@ class VolumeSnapshotsViewTests(test.TestCase):
url = reverse('horizon:project:volumes:volumes:create_snapshot', url = reverse('horizon:project:volumes:volumes:create_snapshot',
args=[volume.id]) args=[volume.id])
res = self.client.post(url, formData) res = self.client.post(url, formData)
self.assertRedirectsNoFollow(res, VOLUME_SNAPSHOTS_TAB_URL) self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({cinder: ('volume_get', @test.create_stubs({cinder: ('volume_get',
'volume_snapshot_create',)}) 'volume_snapshot_create',)})
@ -103,11 +194,10 @@ class VolumeSnapshotsViewTests(test.TestCase):
url = reverse('horizon:project:volumes:volumes:create_snapshot', url = reverse('horizon:project:volumes:volumes:create_snapshot',
args=[volume.id]) args=[volume.id])
res = self.client.post(url, formData) res = self.client.post(url, formData)
self.assertRedirectsNoFollow(res, VOLUME_SNAPSHOTS_TAB_URL) self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api.cinder: ('volume_snapshot_list_paged', @test.create_stubs({api.cinder: ('volume_snapshot_list_paged',
'volume_list', 'volume_list',
'volume_backup_supported',
'volume_snapshot_delete', 'volume_snapshot_delete',
'tenant_absolute_limits')}) 'tenant_absolute_limits')})
def test_delete_volume_snapshot(self): def test_delete_volume_snapshot(self):
@ -115,8 +205,6 @@ class VolumeSnapshotsViewTests(test.TestCase):
volumes = self.cinder_volumes.list() volumes = self.cinder_volumes.list()
snapshot = self.cinder_volume_snapshots.first() snapshot = self.cinder_volume_snapshots.first()
api.cinder.volume_backup_supported(IsA(http.HttpRequest)). \
MultipleTimes().AndReturn(True)
api.cinder.volume_snapshot_list_paged( api.cinder.volume_snapshot_list_paged(
IsA(http.HttpRequest), paginate=True, marker=None, IsA(http.HttpRequest), paginate=True, marker=None,
sort_dir='desc').AndReturn([vol_snapshots, False, False]) sort_dir='desc').AndReturn([vol_snapshots, False, False])
@ -126,11 +214,10 @@ class VolumeSnapshotsViewTests(test.TestCase):
api.cinder.volume_snapshot_delete(IsA(http.HttpRequest), snapshot.id) api.cinder.volume_snapshot_delete(IsA(http.HttpRequest), snapshot.id)
self.mox.ReplayAll() self.mox.ReplayAll()
formData = {'action': formData = {'action': 'volume_snapshots__delete__%s' % snapshot.id}
'volume_snapshots__delete__%s' % snapshot.id} res = self.client.post(INDEX_URL, formData)
res = self.client.post(VOLUME_SNAPSHOTS_TAB_URL, formData)
self.assertRedirectsNoFollow(res, VOLUME_SNAPSHOTS_TAB_URL) self.assertRedirectsNoFollow(res, INDEX_URL)
self.assertMessageCount(success=1) self.assertMessageCount(success=1)
@test.create_stubs({api.cinder: ('volume_snapshot_get', 'volume_get')}) @test.create_stubs({api.cinder: ('volume_snapshot_get', 'volume_get')})
@ -145,7 +232,7 @@ class VolumeSnapshotsViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:volumes:snapshots:detail', url = reverse('horizon:project:snapshots:detail',
args=[snapshot.id]) args=[snapshot.id])
res = self.client.get(url) res = self.client.get(url)
@ -161,7 +248,7 @@ class VolumeSnapshotsViewTests(test.TestCase):
AndRaise(self.exceptions.cinder) AndRaise(self.exceptions.cinder)
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:volumes:snapshots:detail', url = reverse('horizon:project:snapshots:detail',
args=[snapshot.id]) args=[snapshot.id])
res = self.client.get(url) res = self.client.get(url)
@ -180,7 +267,7 @@ class VolumeSnapshotsViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:volumes:snapshots:detail', url = reverse('horizon:project:snapshots:detail',
args=[snapshot.id]) args=[snapshot.id])
res = self.client.get(url) res = self.client.get(url)
@ -203,7 +290,7 @@ class VolumeSnapshotsViewTests(test.TestCase):
formData = {'method': 'UpdateSnapshotForm', formData = {'method': 'UpdateSnapshotForm',
'name': snapshot.name, 'name': snapshot.name,
'description': snapshot.description} 'description': snapshot.description}
url = reverse(('horizon:project:volumes:snapshots:update'), url = reverse(('horizon:project:snapshots:update'),
args=[snapshot.id]) args=[snapshot.id])
res = self.client.post(url, formData) res = self.client.post(url, formData)
self.assertRedirectsNoFollow(res, INDEX_URL) self.assertRedirectsNoFollow(res, INDEX_URL)

View File

@ -12,10 +12,11 @@
from django.conf.urls import url from django.conf.urls import url
from openstack_dashboard.dashboards.project.volumes.snapshots import views from openstack_dashboard.dashboards.project.snapshots import views
urlpatterns = [ urlpatterns = [
url(r'^$', views.SnapshotsView.as_view(), name='index'),
url(r'^(?P<snapshot_id>[^/]+)$', url(r'^(?P<snapshot_id>[^/]+)$',
views.DetailView.as_view(), views.DetailView.as_view(),
name='detail'), name='detail'),

View File

@ -16,26 +16,55 @@ from django.utils.translation import ugettext_lazy as _
from horizon import exceptions from horizon import exceptions
from horizon import forms from horizon import forms
from horizon import tables
from horizon import tabs from horizon import tabs
from horizon.utils import memoized from horizon.utils import memoized
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.dashboards.project.volumes \ from openstack_dashboard.dashboards.project.snapshots \
.snapshots import forms as vol_snapshot_forms import forms as vol_snapshot_forms
from openstack_dashboard.dashboards.project.volumes \ from openstack_dashboard.dashboards.project.snapshots \
.snapshots import tables as vol_snapshot_tables import tables as vol_snapshot_tables
from openstack_dashboard.dashboards.project.volumes \ from openstack_dashboard.dashboards.project.snapshots \
.snapshots import tabs as vol_snapshot_tabs import tabs as vol_snapshot_tabs
class SnapshotsView(tables.DataTableView, tables.PagedTableMixin):
table_class = vol_snapshot_tables.VolumeSnapshotsTable
page_title = _("Volume Snapshots")
def get_data(self):
snapshots = []
volumes = {}
if api.base.is_service_enabled(self.request, 'volumev2'):
try:
marker, sort_dir = self._get_marker()
snapshots, self._has_more_data, self._has_prev_data = \
api.cinder.volume_snapshot_list_paged(
self.request, paginate=True, marker=marker,
sort_dir=sort_dir)
volumes = api.cinder.volume_list(self.request)
volumes = dict((v.id, v) for v in volumes)
except Exception:
raise
exceptions.handle(self.request, _("Unable to retrieve "
"volume snapshots."))
for snapshot in snapshots:
volume = volumes.get(snapshot.volume_id)
setattr(snapshot, '_volume', volume)
return snapshots
class UpdateView(forms.ModalFormView): class UpdateView(forms.ModalFormView):
form_class = vol_snapshot_forms.UpdateForm form_class = vol_snapshot_forms.UpdateForm
form_id = "update_snapshot_form" form_id = "update_snapshot_form"
template_name = 'project/volumes/snapshots/update.html' template_name = 'project/snapshots/update.html'
submit_label = _("Save Changes") submit_label = _("Save Changes")
submit_url = "horizon:project:volumes:snapshots:update" submit_url = "horizon:project:snapshots:update"
success_url = reverse_lazy("horizon:project:volumes:index") success_url = reverse_lazy("horizon:project:snapshots:index")
page_title = _("Edit Snapshot") page_title = _("Edit Snapshot")
@memoized.memoized_method @memoized.memoized_method
@ -46,7 +75,7 @@ class UpdateView(forms.ModalFormView):
snap_id) snap_id)
except Exception: except Exception:
msg = _('Unable to retrieve volume snapshot.') msg = _('Unable to retrieve volume snapshot.')
url = reverse('horizon:project:volumes:index') url = reverse('horizon:project:snapshots:index')
exceptions.handle(self.request, msg, redirect=url) exceptions.handle(self.request, msg, redirect=url)
return self._object return self._object
@ -96,7 +125,7 @@ class DetailView(tabs.TabView):
@staticmethod @staticmethod
def get_redirect_url(): def get_redirect_url():
return reverse('horizon:project:volumes:index') return reverse('horizon:project:snapshots:index')
def get_tabs(self, request, *args, **kwargs): def get_tabs(self, request, *args, **kwargs):
snapshot = self.get_data() snapshot = self.get_data()

View File

@ -17,6 +17,7 @@ from collections import OrderedDict
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from horizon import exceptions from horizon import exceptions
from horizon.tables import PagedTableMixin
from horizon import tabs from horizon import tabs
from openstack_dashboard import api from openstack_dashboard import api
@ -28,8 +29,6 @@ from openstack_dashboard.dashboards.project.volumes.cg_snapshots \
import tables as cg_snapshots_tables import tables as cg_snapshots_tables
from openstack_dashboard.dashboards.project.volumes.cgroups \ from openstack_dashboard.dashboards.project.volumes.cgroups \
import tables as cgroup_tables import tables as cgroup_tables
from openstack_dashboard.dashboards.project.volumes.snapshots \
import tables as vol_snapshot_tables
from openstack_dashboard.dashboards.project.volumes.volumes \ from openstack_dashboard.dashboards.project.volumes.volumes \
import tables as volume_tables import tables as volume_tables
@ -110,30 +109,6 @@ class VolumeTableMixIn(object):
att['instance'] = instances.get(server_id, None) att['instance'] = instances.get(server_id, None)
class PagedTableMixin(object):
def __init__(self, *args, **kwargs):
super(PagedTableMixin, self).__init__(*args, **kwargs)
self._has_prev_data = False
self._has_more_data = False
def has_prev_data(self, table):
return self._has_prev_data
def has_more_data(self, table):
return self._has_more_data
def _get_marker(self):
meta = self.table_classes[0]._meta
prev_marker = self.request.GET.get(meta.prev_pagination_param, None)
if prev_marker:
return prev_marker, "asc"
else:
marker = self.request.GET.get(meta.pagination_param, None)
if marker:
return marker, "desc"
return None, "desc"
class VolumeTab(PagedTableMixin, tabs.TableTab, VolumeTableMixIn): class VolumeTab(PagedTableMixin, tabs.TableTab, VolumeTableMixIn):
table_classes = (volume_tables.VolumesTable,) table_classes = (volume_tables.VolumesTable,)
name = _("Volumes") name = _("Volumes")
@ -151,36 +126,6 @@ class VolumeTab(PagedTableMixin, tabs.TableTab, VolumeTableMixIn):
return volumes return volumes
class SnapshotTab(PagedTableMixin, tabs.TableTab):
table_classes = (vol_snapshot_tables.VolumeSnapshotsTable,)
name = _("Volume Snapshots")
slug = "snapshots_tab"
template_name = ("horizon/common/_detail_table.html")
preload = False
def get_volume_snapshots_data(self):
snapshots = []
volumes = {}
if api.base.is_service_enabled(self.request, 'volumev2'):
try:
marker, sort_dir = self._get_marker()
snapshots, self._has_more_data, self._has_prev_data = \
api.cinder.volume_snapshot_list_paged(
self.request, paginate=True, marker=marker,
sort_dir=sort_dir)
volumes = api.cinder.volume_list(self.request)
volumes = dict((v.id, v) for v in volumes)
except Exception:
exceptions.handle(self.request, _("Unable to retrieve "
"volume snapshots."))
for snapshot in snapshots:
volume = volumes.get(snapshot.volume_id)
setattr(snapshot, '_volume', volume)
return snapshots
class BackupsTab(PagedTableMixin, tabs.TableTab, VolumeTableMixIn): class BackupsTab(PagedTableMixin, tabs.TableTab, VolumeTableMixIn):
table_classes = (backups_tables.BackupsTable,) table_classes = (backups_tables.BackupsTable,)
name = _("Volume Backups") name = _("Volume Backups")
@ -261,5 +206,5 @@ class CGSnapshotsTab(tabs.TableTab):
class VolumeAndSnapshotTabs(tabs.TabGroup): class VolumeAndSnapshotTabs(tabs.TabGroup):
slug = "volumes_and_snapshots" slug = "volumes_and_snapshots"
tabs = (VolumeTab, SnapshotTab, BackupsTab, CGroupsTab, CGSnapshotsTab) tabs = (VolumeTab, BackupsTab, CGroupsTab, CGSnapshotsTab)
sticky = True sticky = True

View File

@ -25,16 +25,12 @@ from mox3.mox import IsA # noqa
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.dashboards.project.volumes.backups \ from openstack_dashboard.dashboards.project.volumes.backups \
import tables as backup_tables import tables as backup_tables
from openstack_dashboard.dashboards.project.volumes.snapshots \
import tables as snapshot_tables
from openstack_dashboard.dashboards.project.volumes.volumes \ from openstack_dashboard.dashboards.project.volumes.volumes \
import tables as volume_tables import tables as volume_tables
from openstack_dashboard.test import helpers as test from openstack_dashboard.test import helpers as test
INDEX_URL = reverse('horizon:project:volumes:index') INDEX_URL = reverse('horizon:project:volumes:index')
VOLUME_SNAPSHOTS_TAB_URL = urlunquote(reverse(
'horizon:project:volumes:snapshots_tab'))
VOLUME_BACKUPS_TAB_URL = urlunquote(reverse( VOLUME_BACKUPS_TAB_URL = urlunquote(reverse(
'horizon:project:volumes:backups_tab')) 'horizon:project:volumes:backups_tab'))
@ -44,7 +40,6 @@ class VolumeAndSnapshotsAndBackupsTests(test.TestCase):
'volume_list', 'volume_list',
'volume_list_paged', 'volume_list_paged',
'volume_snapshot_list', 'volume_snapshot_list',
'volume_snapshot_list_paged',
'volume_backup_supported', 'volume_backup_supported',
'volume_backup_list_paged', 'volume_backup_list_paged',
), ),
@ -67,13 +62,9 @@ class VolumeAndSnapshotsAndBackupsTests(test.TestCase):
api.nova.server_list(IsA(http.HttpRequest), search_opts=None, api.nova.server_list(IsA(http.HttpRequest), search_opts=None,
detailed=False).\ detailed=False).\
AndReturn([self.servers.list(), False]) AndReturn([self.servers.list(), False])
api.cinder.volume_snapshot_list(IsA(http.HttpRequest)).\ api.cinder.volume_snapshot_list(IsA(http.HttpRequest)).\
AndReturn(vol_snaps) AndReturn(vol_snaps)
api.cinder.volume_snapshot_list_paged(
IsA(http.HttpRequest), paginate=True, marker=None,
sort_dir='desc').AndReturn([vol_snaps, False, False])
api.cinder.volume_list(IsA(http.HttpRequest)).AndReturn(volumes)
if backup_supported: if backup_supported:
api.cinder.volume_backup_list_paged( api.cinder.volume_backup_list_paged(
IsA(http.HttpRequest), marker=None, sort_dir='desc', IsA(http.HttpRequest), marker=None, sort_dir='desc',
@ -87,12 +78,6 @@ class VolumeAndSnapshotsAndBackupsTests(test.TestCase):
self.assertEqual(res.status_code, 200) self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(res, 'project/volumes/index.html') self.assertTemplateUsed(res, 'project/volumes/index.html')
# Explicitly load the other tabs. If this doesn't work the test
# will fail due to "Expected methods never called."
res = self.client.get(VOLUME_SNAPSHOTS_TAB_URL)
self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(res, 'project/volumes/index.html')
if backup_supported: if backup_supported:
res = self.client.get(VOLUME_BACKUPS_TAB_URL) res = self.client.get(VOLUME_BACKUPS_TAB_URL)
self.assertTemplateUsed(res, 'project/volumes/index.html') self.assertTemplateUsed(res, 'project/volumes/index.html')
@ -209,95 +194,6 @@ class VolumeAndSnapshotsAndBackupsTests(test.TestCase):
volumes = res.context['volumes_table'].data volumes = res.context['volumes_table'].data
self.assertItemsEqual(volumes, expected_volumes) self.assertItemsEqual(volumes, expected_volumes)
@test.create_stubs({api.cinder: ('tenant_absolute_limits',
'volume_snapshot_list_paged',
'volume_list',
'volume_backup_supported',
),
api.nova: ('server_list',)})
def _test_snapshots_index_paginated(self, marker, sort_dir, snapshots, url,
has_more, has_prev):
backup_supported = True
api.cinder.volume_backup_supported(IsA(http.HttpRequest)).\
MultipleTimes().AndReturn(backup_supported)
api.cinder.volume_snapshot_list_paged(
IsA(http.HttpRequest), marker=marker, sort_dir=sort_dir,
paginate=True).AndReturn([snapshots, has_more, has_prev])
api.cinder.volume_list(IsA(http.HttpRequest)).AndReturn(
self.cinder_volumes.list())
self.mox.ReplayAll()
res = self.client.get(urlunquote(url))
self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(res, 'project/volumes/index.html')
self.mox.UnsetStubs()
return res
@override_settings(API_RESULT_PAGE_SIZE=1)
def test_snapshots_index_paginated(self):
mox_snapshots = self.cinder_volume_snapshots.list()
size = settings.API_RESULT_PAGE_SIZE
base_url = reverse('horizon:project:volumes:snapshots_tab')
next = snapshot_tables.VolumeSnapshotsTable._meta.pagination_param
# get first page
expected_snapshots = mox_snapshots[:size]
res = self._test_snapshots_index_paginated(
marker=None, sort_dir="desc", snapshots=expected_snapshots,
url=base_url, has_more=True, has_prev=False)
snapshots = res.context['volume_snapshots_table'].data
self.assertItemsEqual(snapshots, expected_snapshots)
# get second page
expected_snapshots = mox_snapshots[size:2 * size]
marker = expected_snapshots[0].id
url = "&".join([base_url, "=".join([next, marker])])
res = self._test_snapshots_index_paginated(
marker=marker, sort_dir="desc", snapshots=expected_snapshots,
url=url, has_more=True, has_prev=True)
snapshots = res.context['volume_snapshots_table'].data
self.assertItemsEqual(snapshots, expected_snapshots)
# get last page
expected_snapshots = mox_snapshots[-size:]
marker = expected_snapshots[0].id
url = "&".join([base_url, "=".join([next, marker])])
res = self._test_snapshots_index_paginated(
marker=marker, sort_dir="desc", snapshots=expected_snapshots,
url=url, has_more=False, has_prev=True)
snapshots = res.context['volume_snapshots_table'].data
self.assertItemsEqual(snapshots, expected_snapshots)
@override_settings(API_RESULT_PAGE_SIZE=1)
def test_snapshots_index_paginated_prev_page(self):
mox_snapshots = self.cinder_volume_snapshots.list()
size = settings.API_RESULT_PAGE_SIZE
base_url = reverse('horizon:project:volumes:snapshots_tab')
prev = snapshot_tables.VolumeSnapshotsTable._meta.prev_pagination_param
# prev from some page
expected_snapshots = mox_snapshots[size:2 * size]
marker = expected_snapshots[0].id
url = "&".join([base_url, "=".join([prev, marker])])
res = self._test_snapshots_index_paginated(
marker=marker, sort_dir="asc", snapshots=expected_snapshots,
url=url, has_more=True, has_prev=True)
snapshots = res.context['volume_snapshots_table'].data
self.assertItemsEqual(snapshots, expected_snapshots)
# back to first page
expected_snapshots = mox_snapshots[:size]
marker = expected_snapshots[0].id
url = "&".join([base_url, "=".join([prev, marker])])
res = self._test_snapshots_index_paginated(
marker=marker, sort_dir="asc", snapshots=expected_snapshots,
url=url, has_more=True, has_prev=False)
snapshots = res.context['volume_snapshots_table'].data
self.assertItemsEqual(snapshots, expected_snapshots)
@test.create_stubs({api.cinder: ('tenant_absolute_limits', @test.create_stubs({api.cinder: ('tenant_absolute_limits',
'volume_backup_list_paged', 'volume_backup_list_paged',
'volume_list', 'volume_list',

View File

@ -21,16 +21,12 @@ from openstack_dashboard.dashboards.project.volumes.cg_snapshots \
import urls as cg_snapshots_urls import urls as cg_snapshots_urls
from openstack_dashboard.dashboards.project.volumes.cgroups \ from openstack_dashboard.dashboards.project.volumes.cgroups \
import urls as cgroup_urls import urls as cgroup_urls
from openstack_dashboard.dashboards.project.volumes.snapshots \
import urls as snapshot_urls
from openstack_dashboard.dashboards.project.volumes import views from openstack_dashboard.dashboards.project.volumes import views
from openstack_dashboard.dashboards.project.volumes.volumes \ from openstack_dashboard.dashboards.project.volumes.volumes \
import urls as volume_urls import urls as volume_urls
urlpatterns = [ urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'), url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^\?tab=volumes_and_snapshots__snapshots_tab$',
views.IndexView.as_view(), name='snapshots_tab'),
url(r'^\?tab=volumes_and_snapshots__volumes_tab$', url(r'^\?tab=volumes_and_snapshots__volumes_tab$',
views.IndexView.as_view(), name='volumes_tab'), views.IndexView.as_view(), name='volumes_tab'),
url(r'^\?tab=volumes_and_snapshots__backups_tab$', url(r'^\?tab=volumes_and_snapshots__backups_tab$',
@ -45,9 +41,6 @@ urlpatterns = [
url(r'backups/', include( url(r'backups/', include(
backups_urls, backups_urls,
namespace='backups')), namespace='backups')),
url(r'snapshots/', include(
snapshot_urls,
namespace='snapshots')),
url(r'cgroups/', include( url(r'cgroups/', include(
cgroup_urls, cgroup_urls,
namespace='cgroups')), namespace='cgroups')),

View File

@ -191,7 +191,7 @@ class CreateSnapshotView(forms.ModalFormView):
form_class = project_forms.CreateSnapshotForm form_class = project_forms.CreateSnapshotForm
template_name = 'project/volumes/volumes/create_snapshot.html' template_name = 'project/volumes/volumes/create_snapshot.html'
submit_url = "horizon:project:volumes:volumes:create_snapshot" submit_url = "horizon:project:volumes:volumes:create_snapshot"
success_url = reverse_lazy('horizon:project:volumes:snapshots_tab') success_url = reverse_lazy('horizon:project:snapshots:index')
page_title = _("Create Volume Snapshot") page_title = _("Create Volume Snapshot")
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):

View File

@ -0,0 +1,8 @@
from django.utils.translation import ugettext_lazy as _
# The slug of the panel group to be added to HORIZON_CONFIG. Required.
PANEL_GROUP = 'volumes'
# The display name of the PANEL_GROUP. Required.
PANEL_GROUP_NAME = _('Volumes')
# The slug of the dashboard the PANEL_GROUP associated with. Required.
PANEL_GROUP_DASHBOARD = 'project'

View File

@ -0,0 +1,9 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'snapshots'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'project'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'volumes'
# Python panel class of the PANEL to be added.
ADD_PANEL = 'openstack_dashboard.dashboards.project.snapshots.panel.Snapshots'