Move Volume Backups out of tabbed panel

Notes on enabling backup:
https://github.com/coolsvap/devstack-cinder-backup

Change-Id: Ie6bcfad30d04ee35c75d693f5637197297ca84ef
Implements: blueprint reorganise-volumes
This commit is contained in:
Richard Jones 2017-02-03 16:52:55 +11:00
parent d7e1adeca0
commit 921f84a7ce
18 changed files with 183 additions and 183 deletions

View File

@ -74,7 +74,7 @@ class RestoreBackupForm(forms.SelfHandlingForm):
volumes = api.cinder.volume_list(request)
except Exception:
msg = _('Unable to lookup volume or backup information.')
redirect = reverse('horizon:project:volumes:index')
redirect = reverse('horizon:project:backups:index')
exceptions.handle(request, msg, redirect=redirect)
raise exceptions.Http302(redirect)
@ -104,5 +104,5 @@ class RestoreBackupForm(forms.SelfHandlingForm):
return restore
except Exception:
msg = _('Unable to restore backup.')
redirect = reverse('horizon:project:volumes:index')
redirect = reverse('horizon:project:backups:index')
exceptions.handle(request, msg, 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 Backups(horizon.Panel):
name = _("Backups")
slug = 'backups'
permissions = (
('openstack.services.volume', 'openstack.services.volumev2'),
)
policy_rules = (("volume", "backup:get_all"),)

View File

@ -86,7 +86,7 @@ class RestoreBackup(tables.LinkAction):
backup_id = datum.id
backup_name = datum.name
volume_id = getattr(datum, 'volume_id', None)
url = reverse("horizon:project:volumes:backups:restore",
url = reverse("horizon:project:backups:restore",
args=(backup_id,))
url += '?%s' % http.urlencode({'backup_name': backup_name,
'volume_id': volume_id})
@ -133,7 +133,7 @@ class BackupsTable(tables.DataTable):
)
name = tables.Column("name",
verbose_name=_("Name"),
link="horizon:project:volumes:backups:detail")
link="horizon:project:backups:detail")
description = tables.Column("description",
verbose_name=_("Description"),
truncate=40)

View File

@ -23,8 +23,7 @@ from openstack_dashboard.api import cinder
class BackupOverviewTab(tabs.Tab):
name = _("Overview")
slug = "overview"
template_name = ("project/volumes/backups/"
"_detail_overview.html")
template_name = "project/backups/_detail_overview.html"
def get_context_data(self, request):
try:
@ -36,7 +35,7 @@ class BackupOverviewTab(tabs.Tab):
return {'backup': backup,
'volume': volume}
except Exception:
redirect = reverse('horizon:project:volumes:index')
redirect = reverse('horizon:project:backups:index')
exceptions.handle(self.request,
_('Unable to retrieve backup details.'),
redirect=redirect)

View File

@ -10,21 +10,108 @@
# License for the specific language governing permissions and limitations
# under the License.
from django.conf import settings
from django.core.urlresolvers import reverse
from django import http
from django.test.utils import override_settings
from django.utils.http import urlencode
from django.utils.http import urlunquote
from mox3.mox import IsA # noqa
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.backups \
import tables as backup_tables
from openstack_dashboard.test import helpers as test
INDEX_URL = reverse('horizon:project:volumes:index')
VOLUME_BACKUPS_TAB_URL = reverse('horizon:project:volumes:backups_tab')
INDEX_URL = reverse('horizon:project:backups:index')
class VolumeBackupsViewTests(test.TestCase):
@test.create_stubs({api.cinder: ('tenant_absolute_limits',
'volume_backup_list_paged',
'volume_list'),
api.nova: ('server_list',)})
def _test_backups_index_paginated(self, marker, sort_dir, backups, url,
has_more, has_prev):
api.cinder.volume_backup_list_paged(
IsA(http.HttpRequest), marker=marker, sort_dir=sort_dir,
paginate=True).AndReturn([backups, 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_backups_index_paginated(self):
mox_backups = self.cinder_volume_backups.list()
size = settings.API_RESULT_PAGE_SIZE
base_url = INDEX_URL
next = backup_tables.BackupsTable._meta.pagination_param
# get first page
expected_backups = mox_backups[:size]
res = self._test_backups_index_paginated(
marker=None, sort_dir="desc", backups=expected_backups,
url=base_url, has_more=True, has_prev=False)
backups = res.context['volume_backups_table'].data
self.assertItemsEqual(backups, expected_backups)
# get second page
expected_backups = mox_backups[size:2 * size]
marker = expected_backups[0].id
url = base_url + "?%s=%s" % (next, marker)
res = self._test_backups_index_paginated(
marker=marker, sort_dir="desc", backups=expected_backups, url=url,
has_more=True, has_prev=True)
backups = res.context['volume_backups_table'].data
self.assertItemsEqual(backups, expected_backups)
# get last page
expected_backups = mox_backups[-size:]
marker = expected_backups[0].id
url = base_url + "?%s=%s" % (next, marker)
res = self._test_backups_index_paginated(
marker=marker, sort_dir="desc", backups=expected_backups, url=url,
has_more=False, has_prev=True)
backups = res.context['volume_backups_table'].data
self.assertItemsEqual(backups, expected_backups)
@override_settings(API_RESULT_PAGE_SIZE=1)
def test_backups_index_paginated_prev_page(self):
mox_backups = self.cinder_volume_backups.list()
size = settings.API_RESULT_PAGE_SIZE
base_url = INDEX_URL
prev = backup_tables.BackupsTable._meta.prev_pagination_param
# prev from some page
expected_backups = mox_backups[size:2 * size]
marker = expected_backups[0].id
url = base_url + "?%s=%s" % (prev, marker)
res = self._test_backups_index_paginated(
marker=marker, sort_dir="asc", backups=expected_backups, url=url,
has_more=True, has_prev=True)
backups = res.context['volume_backups_table'].data
self.assertItemsEqual(backups, expected_backups)
# back to first page
expected_backups = mox_backups[:size]
marker = expected_backups[0].id
url = base_url + "?%s=%s" % (prev, marker)
res = self._test_backups_index_paginated(
marker=marker, sort_dir="asc", backups=expected_backups, url=url,
has_more=True, has_prev=False)
backups = res.context['volume_backups_table'].data
self.assertItemsEqual(backups, expected_backups)
@test.create_stubs({api.cinder: ('volume_backup_create',)})
def test_create_backup_post(self):
volume = self.volumes.first()
@ -50,10 +137,9 @@ class VolumeBackupsViewTests(test.TestCase):
self.assertNoFormErrors(res)
self.assertMessageCount(error=0, warning=0)
self.assertRedirectsNoFollow(res, VOLUME_BACKUPS_TAB_URL)
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api.cinder: ('volume_list',
'volume_backup_supported',
'volume_backup_list_paged',
'volume_backup_delete')})
def test_delete_volume_backup(self):
@ -61,8 +147,6 @@ class VolumeBackupsViewTests(test.TestCase):
volumes = self.cinder_volumes.list()
backup = self.cinder_volume_backups.first()
api.cinder.volume_backup_supported(IsA(http.HttpRequest)). \
MultipleTimes().AndReturn(True)
api.cinder.volume_backup_list_paged(
IsA(http.HttpRequest), marker=None, sort_dir='desc',
paginate=True).AndReturn([vol_backups, False, False])
@ -74,12 +158,9 @@ class VolumeBackupsViewTests(test.TestCase):
formData = {'action':
'volume_backups__delete__%s' % backup.id}
res = self.client.post(INDEX_URL +
"?tab=volumes_and_snapshots__backups_tab",
formData)
res = self.client.post(INDEX_URL, formData)
self.assertRedirectsNoFollow(res, INDEX_URL +
"?tab=volumes_and_snapshots__backups_tab")
self.assertRedirectsNoFollow(res, INDEX_URL)
self.assertMessageCount(success=1)
@test.create_stubs({api.cinder: ('volume_backup_get', 'volume_get')})
@ -93,7 +174,7 @@ class VolumeBackupsViewTests(test.TestCase):
AndReturn(volume)
self.mox.ReplayAll()
url = reverse('horizon:project:volumes:backups:detail',
url = reverse('horizon:project:backups:detail',
args=[backup.id])
res = self.client.get(url)
@ -109,7 +190,7 @@ class VolumeBackupsViewTests(test.TestCase):
AndRaise(self.exceptions.cinder)
self.mox.ReplayAll()
url = reverse('horizon:project:volumes:backups:detail',
url = reverse('horizon:project:backups:detail',
args=[backup.id])
res = self.client.get(url)
@ -128,7 +209,7 @@ class VolumeBackupsViewTests(test.TestCase):
AndRaise(self.exceptions.cinder)
self.mox.ReplayAll()
url = reverse('horizon:project:volumes:backups:detail',
url = reverse('horizon:project:backups:detail',
args=[backup.id])
res = self.client.get(url)
@ -153,7 +234,7 @@ class VolumeBackupsViewTests(test.TestCase):
'backup_id': backup.id,
'backup_name': backup.name,
'volume_id': backup.volume_id}
url = reverse('horizon:project:volumes:backups:restore',
url = reverse('horizon:project:backups:restore',
args=[backup.id])
url += '?%s' % urlencode({'backup_name': backup.name,
'volume_id': backup.volume_id})
@ -161,4 +242,5 @@ class VolumeBackupsViewTests(test.TestCase):
self.assertNoFormErrors(res)
self.assertMessageCount(info=1)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.assertRedirectsNoFollow(res,
reverse('horizon:project:volumes:index'))

View File

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

View File

@ -16,24 +16,53 @@ from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import tables
from horizon import tabs
from horizon.utils import memoized
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.volumes.backups \
from openstack_dashboard.dashboards.project.backups \
import forms as backup_forms
from openstack_dashboard.dashboards.project.volumes.backups \
from openstack_dashboard.dashboards.project.backups \
import tables as backup_tables
from openstack_dashboard.dashboards.project.volumes.backups \
from openstack_dashboard.dashboards.project.backups \
import tabs as backup_tabs
from openstack_dashboard.dashboards.project.volumes \
import tabs as volume_tabs
class BackupsView(tables.DataTableView, tables.PagedTableMixin,
volume_tabs.VolumeTableMixIn):
table_class = backup_tables.BackupsTable
page_title = _("Volume Backups")
def allowed(self, request):
return api.cinder.volume_backup_supported(self.request)
def get_data(self):
try:
marker, sort_dir = self._get_marker()
backups, self._has_more_data, self._has_prev_data = \
api.cinder.volume_backup_list_paged(
self.request, marker=marker, sort_dir=sort_dir,
paginate=True)
volumes = api.cinder.volume_list(self.request)
volumes = dict((v.id, v) for v in volumes)
for backup in backups:
backup.volume = volumes.get(backup.volume_id)
except Exception:
backups = []
exceptions.handle(self.request, _("Unable to retrieve "
"volume backups."))
return backups
class CreateBackupView(forms.ModalFormView):
form_class = backup_forms.CreateBackupForm
template_name = 'project/volumes/backups/create_backup.html'
template_name = 'project/backups/create_backup.html'
submit_label = _("Create Volume Backup")
submit_url = "horizon:project:volumes:volumes:create_backup"
success_url = reverse_lazy("horizon:project:volumes:backups_tab")
success_url = reverse_lazy("horizon:project:backups:index")
page_title = _("Create Volume Backup")
def get_context_data(self, **kwargs):
@ -79,14 +108,14 @@ class BackupDetailView(tabs.TabView):
@staticmethod
def get_redirect_url():
return reverse('horizon:project:volumes:index')
return reverse('horizon:project:backups:index')
class RestoreBackupView(forms.ModalFormView):
form_class = backup_forms.RestoreBackupForm
template_name = 'project/volumes/backups/restore_backup.html'
template_name = 'project/backups/restore_backup.html'
submit_label = _("Restore Backup to Volume")
submit_url = "horizon:project:volumes:backups:restore"
submit_url = "horizon:project:backups:restore"
success_url = reverse_lazy('horizon:project:volumes:index')
page_title = _("Restore Volume Backup")

View File

@ -23,8 +23,6 @@ from horizon import tabs
from openstack_dashboard import api
from openstack_dashboard import policy
from openstack_dashboard.dashboards.project.volumes.backups \
import tables as backups_tables
from openstack_dashboard.dashboards.project.volumes.cg_snapshots \
import tables as cg_snapshots_tables
from openstack_dashboard.dashboards.project.volumes.cgroups \
@ -126,34 +124,6 @@ class VolumeTab(PagedTableMixin, tabs.TableTab, VolumeTableMixIn):
return volumes
class BackupsTab(PagedTableMixin, tabs.TableTab, VolumeTableMixIn):
table_classes = (backups_tables.BackupsTable,)
name = _("Volume Backups")
slug = "backups_tab"
template_name = ("horizon/common/_detail_table.html")
preload = False
def allowed(self, request):
return api.cinder.volume_backup_supported(self.request)
def get_volume_backups_data(self):
try:
marker, sort_dir = self._get_marker()
backups, self._has_more_data, self._has_prev_data = \
api.cinder.volume_backup_list_paged(
self.request, marker=marker, sort_dir=sort_dir,
paginate=True)
volumes = api.cinder.volume_list(self.request)
volumes = dict((v.id, v) for v in volumes)
for backup in backups:
backup.volume = volumes.get(backup.volume_id)
except Exception:
backups = []
exceptions.handle(self.request, _("Unable to retrieve "
"volume backups."))
return backups
class CGroupsTab(tabs.TableTab):
table_classes = (cgroup_tables.VolumeCGroupsTable,)
name = _("Consistency Groups")
@ -206,5 +176,5 @@ class CGSnapshotsTab(tabs.TableTab):
class VolumeAndSnapshotTabs(tabs.TabGroup):
slug = "volumes_and_snapshots"
tabs = (VolumeTab, BackupsTab, CGroupsTab, CGSnapshotsTab)
tabs = (VolumeTab, CGroupsTab, CGSnapshotsTab)
sticky = True

View File

@ -23,16 +23,12 @@ from django.utils.http import urlunquote
from mox3.mox import IsA # noqa
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.volumes.backups \
import tables as backup_tables
from openstack_dashboard.dashboards.project.volumes.volumes \
import tables as volume_tables
from openstack_dashboard.test import helpers as test
INDEX_URL = reverse('horizon:project:volumes:index')
VOLUME_BACKUPS_TAB_URL = urlunquote(reverse(
'horizon:project:volumes:backups_tab'))
class VolumeAndSnapshotsAndBackupsTests(test.TestCase):
@ -44,8 +40,7 @@ class VolumeAndSnapshotsAndBackupsTests(test.TestCase):
'volume_backup_list_paged',
),
api.nova: ('server_list',)})
def _test_index(self, backup_supported=True, instanceless_volumes=False):
vol_backups = self.cinder_volume_backups.list()
def test_index(self, instanceless_volumes=False):
vol_snaps = self.cinder_volume_snapshots.list()
volumes = self.cinder_volumes.list()
if instanceless_volumes:
@ -53,7 +48,7 @@ class VolumeAndSnapshotsAndBackupsTests(test.TestCase):
volume.attachments = []
api.cinder.volume_backup_supported(IsA(http.HttpRequest)).\
MultipleTimes().AndReturn(backup_supported)
MultipleTimes().AndReturn(False)
api.cinder.volume_list_paged(
IsA(http.HttpRequest), marker=None, search_opts=None,
sort_dir='desc', paginate=True).\
@ -65,11 +60,6 @@ class VolumeAndSnapshotsAndBackupsTests(test.TestCase):
api.cinder.volume_snapshot_list(IsA(http.HttpRequest)).\
AndReturn(vol_snaps)
if backup_supported:
api.cinder.volume_backup_list_paged(
IsA(http.HttpRequest), marker=None, sort_dir='desc',
paginate=True).AndReturn([vol_backups, False, False])
api.cinder.volume_list(IsA(http.HttpRequest)).AndReturn(volumes)
api.cinder.tenant_absolute_limits(IsA(http.HttpRequest)).\
MultipleTimes().AndReturn(self.cinder_limits['absolute'])
self.mox.ReplayAll()
@ -78,18 +68,8 @@ class VolumeAndSnapshotsAndBackupsTests(test.TestCase):
self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(res, 'project/volumes/index.html')
if backup_supported:
res = self.client.get(VOLUME_BACKUPS_TAB_URL)
self.assertTemplateUsed(res, 'project/volumes/index.html')
def test_index_backup_supported(self):
self._test_index(backup_supported=True)
def test_index_backup_not_supported(self):
self._test_index(backup_supported=False)
def test_index_no_volume_attachments(self):
self._test_index(instanceless_volumes=True)
self.test_index(instanceless_volumes=True)
@test.create_stubs({api.cinder: ('tenant_absolute_limits',
'volume_list_paged',
@ -193,92 +173,3 @@ class VolumeAndSnapshotsAndBackupsTests(test.TestCase):
has_more=True, has_prev=False)
volumes = res.context['volumes_table'].data
self.assertItemsEqual(volumes, expected_volumes)
@test.create_stubs({api.cinder: ('tenant_absolute_limits',
'volume_backup_list_paged',
'volume_list',
'volume_backup_supported',
),
api.nova: ('server_list',)})
def _test_backups_index_paginated(self, marker, sort_dir, backups, url,
has_more, has_prev):
backup_supported = True
api.cinder.volume_backup_supported(IsA(http.HttpRequest)).\
MultipleTimes().AndReturn(backup_supported)
api.cinder.volume_backup_list_paged(
IsA(http.HttpRequest), marker=marker, sort_dir=sort_dir,
paginate=True).AndReturn([backups, 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_backups_index_paginated(self):
mox_backups = self.cinder_volume_backups.list()
size = settings.API_RESULT_PAGE_SIZE
base_url = reverse('horizon:project:volumes:backups_tab')
next = backup_tables.BackupsTable._meta.pagination_param
# get first page
expected_backups = mox_backups[:size]
res = self._test_backups_index_paginated(
marker=None, sort_dir="desc", backups=expected_backups,
url=base_url, has_more=True, has_prev=False)
backups = res.context['volume_backups_table'].data
self.assertItemsEqual(backups, expected_backups)
# get second page
expected_backups = mox_backups[size:2 * size]
marker = expected_backups[0].id
url = "&".join([base_url, "=".join([next, marker])])
res = self._test_backups_index_paginated(
marker=marker, sort_dir="desc", backups=expected_backups, url=url,
has_more=True, has_prev=True)
backups = res.context['volume_backups_table'].data
self.assertItemsEqual(backups, expected_backups)
# get last page
expected_backups = mox_backups[-size:]
marker = expected_backups[0].id
url = "&".join([base_url, "=".join([next, marker])])
res = self._test_backups_index_paginated(
marker=marker, sort_dir="desc", backups=expected_backups, url=url,
has_more=False, has_prev=True)
backups = res.context['volume_backups_table'].data
self.assertItemsEqual(backups, expected_backups)
@override_settings(API_RESULT_PAGE_SIZE=1)
def test_backups_index_paginated_prev_page(self):
mox_backups = self.cinder_volume_backups.list()
size = settings.API_RESULT_PAGE_SIZE
base_url = reverse('horizon:project:volumes:backups_tab')
prev = backup_tables.BackupsTable._meta.prev_pagination_param
# prev from some page
expected_backups = mox_backups[size:2 * size]
marker = expected_backups[0].id
url = "&".join([base_url, "=".join([prev, marker])])
res = self._test_backups_index_paginated(
marker=marker, sort_dir="asc", backups=expected_backups, url=url,
has_more=True, has_prev=True)
backups = res.context['volume_backups_table'].data
self.assertItemsEqual(backups, expected_backups)
# back to first page
expected_backups = mox_backups[:size]
marker = expected_backups[0].id
url = "&".join([base_url, "=".join([prev, marker])])
res = self._test_backups_index_paginated(
marker=marker, sort_dir="asc", backups=expected_backups, url=url,
has_more=True, has_prev=False)
backups = res.context['volume_backups_table'].data
self.assertItemsEqual(backups, expected_backups)

View File

@ -15,8 +15,6 @@
from django.conf.urls import include
from django.conf.urls import url
from openstack_dashboard.dashboards.project.volumes.backups \
import urls as backups_urls
from openstack_dashboard.dashboards.project.volumes.cg_snapshots \
import urls as cg_snapshots_urls
from openstack_dashboard.dashboards.project.volumes.cgroups \
@ -29,8 +27,6 @@ urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^\?tab=volumes_and_snapshots__volumes_tab$',
views.IndexView.as_view(), name='volumes_tab'),
url(r'^\?tab=volumes_and_snapshots__backups_tab$',
views.IndexView.as_view(), name='backups_tab'),
url(r'^\?tab=volumes_and_snapshots__cgroups_tab$',
views.IndexView.as_view(), name='cgroups_tab'),
url(r'^\?tab=volumes_and_snapshots__cg_snapshots_tab$',
@ -38,9 +34,6 @@ urlpatterns = [
url(r'', include(
volume_urls,
namespace='volumes')),
url(r'backups/', include(
backups_urls,
namespace='backups')),
url(r'cgroups/', include(
cgroup_urls,
namespace='cgroups')),

View File

@ -16,7 +16,7 @@ from django.conf.urls import url
from openstack_dashboard.dashboards.project.volumes \
.volumes import views
from openstack_dashboard.dashboards.project.volumes.backups \
from openstack_dashboard.dashboards.project.backups \
import views as backup_views

View File

@ -0,0 +1,9 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'backups'
# 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.backups.panel.Backups'