Merge "Adding support for volume backups"
This commit is contained in:
commit
1ddd7f0e92
@ -423,6 +423,17 @@ are using HTTPS, running your Keystone server on a nonstandard port, or using
|
||||
a nonstandard URL scheme you shouldn't need to touch this setting.
|
||||
|
||||
|
||||
``OPENSTACK_CINDER_FEATURES``
|
||||
-----------------------------
|
||||
|
||||
.. versionadded:: 2014.2(Juno)
|
||||
|
||||
Default: ``{'enable_backup': False}``
|
||||
|
||||
A dictionary of settings which can be used to enable optional services provided
|
||||
by cinder. Currently only the backup service is available.
|
||||
|
||||
|
||||
``OPENSTACK_NEUTRON_NETWORK``
|
||||
-----------------------------
|
||||
|
||||
|
@ -93,6 +93,21 @@ class VolumeSnapshot(BaseCinderAPIResourceWrapper):
|
||||
'os-extended-snapshot-attributes:project_id']
|
||||
|
||||
|
||||
class VolumeBackup(BaseCinderAPIResourceWrapper):
|
||||
|
||||
_attrs = ['id', 'name', 'description', 'container', 'size', 'status',
|
||||
'created_at', 'volume_id', 'availability_zone']
|
||||
_volume = None
|
||||
|
||||
@property
|
||||
def volume(self):
|
||||
return self._volume
|
||||
|
||||
@volume.setter
|
||||
def volume(self, value):
|
||||
self._volume = value
|
||||
|
||||
|
||||
def cinderclient(request):
|
||||
api_version = VERSIONS.get_active_version()
|
||||
|
||||
@ -232,6 +247,52 @@ def volume_snapshot_update(request, snapshot_id, name, description):
|
||||
**snapshot_data)
|
||||
|
||||
|
||||
@memoized
|
||||
def volume_backup_supported(request):
|
||||
"""This method will determine if cinder supports backup.
|
||||
"""
|
||||
# TODO(lcheng) Cinder does not expose the information if cinder
|
||||
# backup is configured yet. This is a workaround until that
|
||||
# capability is available.
|
||||
# https://bugs.launchpad.net/cinder/+bug/1334856
|
||||
cinder_config = getattr(settings, 'OPENSTACK_CINDER_FEATURES', {})
|
||||
return cinder_config.get('enable_backup', False)
|
||||
|
||||
|
||||
def volume_backup_get(request, backup_id):
|
||||
backup = cinderclient(request).backups.get(backup_id)
|
||||
return VolumeBackup(backup)
|
||||
|
||||
|
||||
def volume_backup_list(request):
|
||||
c_client = cinderclient(request)
|
||||
if c_client is None:
|
||||
return []
|
||||
return [VolumeBackup(b) for b in c_client.backups.list()]
|
||||
|
||||
|
||||
def volume_backup_create(request,
|
||||
volume_id,
|
||||
container_name,
|
||||
name,
|
||||
description):
|
||||
backup = cinderclient(request).backups.create(
|
||||
volume_id,
|
||||
container=container_name,
|
||||
name=name,
|
||||
description=description)
|
||||
return VolumeBackup(backup)
|
||||
|
||||
|
||||
def volume_backup_delete(request, backup_id):
|
||||
return cinderclient(request).backups.delete(backup_id)
|
||||
|
||||
|
||||
def volume_backup_restore(request, backup_id, volume_id):
|
||||
return cinderclient(request).restores.restore(backup_id=backup_id,
|
||||
volume_id=volume_id)
|
||||
|
||||
|
||||
def tenant_quota_get(request, tenant_id):
|
||||
c_client = cinderclient(request)
|
||||
if c_client is None:
|
||||
|
@ -49,11 +49,11 @@
|
||||
"volume:delete_transfer": [],
|
||||
"volume:get_all_transfers": [],
|
||||
|
||||
"backup:create" : [],
|
||||
"backup:delete": [],
|
||||
"backup:create" : ["rule:default"],
|
||||
"backup:delete": ["rule:default"],
|
||||
"backup:get": [],
|
||||
"backup:get_all": [],
|
||||
"backup:restore": [],
|
||||
"backup:restore": ["rule:default"],
|
||||
|
||||
"snapshot_extension:snapshot_actions:update_snapshot_status": []
|
||||
}
|
||||
|
109
openstack_dashboard/dashboards/project/volumes/backups/forms.py
Normal file
109
openstack_dashboard/dashboards/project/volumes/backups/forms.py
Normal file
@ -0,0 +1,109 @@
|
||||
# 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.
|
||||
|
||||
|
||||
"""
|
||||
Views for managing backups.
|
||||
"""
|
||||
|
||||
import operator
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import messages
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.dashboards.project.containers.forms \
|
||||
import no_slash_validator
|
||||
|
||||
|
||||
class CreateBackupForm(forms.SelfHandlingForm):
|
||||
name = forms.CharField(max_length="255", label=_("Backup Name"))
|
||||
description = forms.CharField(widget=forms.Textarea,
|
||||
label=_("Description"),
|
||||
required=False)
|
||||
container_name = forms.CharField(max_length="255",
|
||||
label=_("Container Name"),
|
||||
validators=[no_slash_validator],
|
||||
required=False)
|
||||
volume_id = forms.CharField(widget=forms.HiddenInput())
|
||||
|
||||
def handle(self, request, data):
|
||||
# Create a container for the user if no input is given
|
||||
if not data['container_name']:
|
||||
data['container_name'] = 'volumebackups'
|
||||
|
||||
try:
|
||||
backup = api.cinder.volume_backup_create(request,
|
||||
data['volume_id'],
|
||||
data['container_name'],
|
||||
data['name'],
|
||||
data['description'])
|
||||
|
||||
message = _('Creating volume backup "%s"') % data['name']
|
||||
messages.success(request, message)
|
||||
return backup
|
||||
|
||||
except Exception:
|
||||
redirect = reverse('horizon:project:volumes:index')
|
||||
exceptions.handle(request,
|
||||
_('Unable to create volume backup.'),
|
||||
redirect=redirect)
|
||||
return False
|
||||
|
||||
|
||||
class RestoreBackupForm(forms.SelfHandlingForm):
|
||||
volume_id = forms.ChoiceField(label=_('Select Volume'), required=False)
|
||||
backup_id = forms.CharField(widget=forms.HiddenInput())
|
||||
backup_name = forms.CharField(widget=forms.HiddenInput())
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(RestoreBackupForm, self).__init__(request, *args, **kwargs)
|
||||
|
||||
try:
|
||||
volumes = api.cinder.volume_list(request)
|
||||
except Exception:
|
||||
msg = _('Unable to lookup volume or backup information.')
|
||||
redirect = reverse('horizon:project:volumes:index')
|
||||
exceptions.handle(request, msg, redirect=redirect)
|
||||
raise exceptions.Http302(redirect)
|
||||
|
||||
volumes.sort(key=operator.attrgetter('name', 'created_at'))
|
||||
choices = [('', _('Create a New Volume'))]
|
||||
choices.extend((volume.id, volume.name) for volume in volumes)
|
||||
self.fields['volume_id'].choices = choices
|
||||
|
||||
def handle(self, request, data):
|
||||
backup_id = data['backup_id']
|
||||
backup_name = data['backup_name'] or None
|
||||
volume_id = data['volume_id'] or None
|
||||
|
||||
try:
|
||||
restore = api.cinder.volume_backup_restore(request,
|
||||
backup_id,
|
||||
volume_id)
|
||||
|
||||
# Needed for cases when a new volume is created.
|
||||
volume_id = restore.volume_id
|
||||
|
||||
message = _('Successfully restored backup %(backup_name)s '
|
||||
'to volume with id: %(volume_id)s')
|
||||
messages.success(request, message % {'backup_name': backup_name,
|
||||
'volume_id': volume_id})
|
||||
return restore
|
||||
except Exception:
|
||||
msg = _('Unable to restore backup.')
|
||||
exceptions.handle(request, msg)
|
||||
return False
|
130
openstack_dashboard/dashboards/project/volumes/backups/tables.py
Normal file
130
openstack_dashboard/dashboards/project/volumes/backups/tables.py
Normal file
@ -0,0 +1,130 @@
|
||||
# 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.template.defaultfilters import title # noqa
|
||||
from django.utils import html
|
||||
from django.utils import http
|
||||
from django.utils import safestring
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import tables
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.api import cinder
|
||||
|
||||
|
||||
DELETABLE_STATES = ("available", "error",)
|
||||
|
||||
|
||||
class BackupVolumeNameColumn(tables.Column):
|
||||
def get_raw_data(self, backup):
|
||||
volume = backup.volume
|
||||
if volume:
|
||||
volume_name = volume.name
|
||||
volume_name = html.escape(volume_name)
|
||||
else:
|
||||
volume_name = _("Unknown")
|
||||
return safestring.mark_safe(volume_name)
|
||||
|
||||
def get_link_url(self, backup):
|
||||
volume = backup.volume
|
||||
if volume:
|
||||
volume_id = volume.id
|
||||
return reverse(self.link, args=(volume_id,))
|
||||
|
||||
|
||||
class DeleteBackup(tables.DeleteAction):
|
||||
data_type_singular = _("Volume Backup")
|
||||
data_type_plural = _("Volume Backups")
|
||||
action_past = _("Scheduled deletion of")
|
||||
policy_rules = (("volume", "backup:delete"),)
|
||||
|
||||
def delete(self, request, obj_id):
|
||||
api.cinder.volume_backup_delete(request, obj_id)
|
||||
|
||||
def allowed(self, request, volume=None):
|
||||
if volume:
|
||||
return volume.status in DELETABLE_STATES
|
||||
return True
|
||||
|
||||
|
||||
class RestoreBackup(tables.LinkAction):
|
||||
name = "restore"
|
||||
verbose_name = _("Restore Backup")
|
||||
classes = ("ajax-modal",)
|
||||
policy_rules = (("volume", "backup:restore"),)
|
||||
|
||||
def allowed(self, request, volume=None):
|
||||
return volume.status == "available"
|
||||
|
||||
def get_link_url(self, datum):
|
||||
backup_id = datum.id
|
||||
backup_name = datum.name
|
||||
volume_id = getattr(datum, 'volume_id', None)
|
||||
url = reverse("horizon:project:volumes:backups:restore",
|
||||
args=(backup_id,))
|
||||
url += '?%s' % http.urlencode({'backup_name': backup_name,
|
||||
'volume_id': volume_id})
|
||||
return url
|
||||
|
||||
|
||||
class UpdateRow(tables.Row):
|
||||
ajax = True
|
||||
|
||||
def get_data(self, request, backup_id):
|
||||
backup = cinder.volume_backup_get(request, backup_id)
|
||||
try:
|
||||
backup.volume = cinder.volume_get(request,
|
||||
backup.volume_id)
|
||||
except Exception:
|
||||
pass
|
||||
return backup
|
||||
|
||||
|
||||
def get_size(backup):
|
||||
return _("%sGB") % backup.size
|
||||
|
||||
|
||||
class BackupsTable(tables.DataTable):
|
||||
STATUS_CHOICES = (
|
||||
("available", True),
|
||||
("creating", None),
|
||||
("restoring", None),
|
||||
("error", False),
|
||||
)
|
||||
name = tables.Column("name",
|
||||
verbose_name=_("Name"),
|
||||
link="horizon:project:volumes:backups:detail")
|
||||
description = tables.Column("description",
|
||||
verbose_name=_("Description"),
|
||||
truncate=40)
|
||||
size = tables.Column(get_size,
|
||||
verbose_name=_("Size"),
|
||||
attrs={'data-type': 'size'})
|
||||
status = tables.Column("status",
|
||||
filters=(title,),
|
||||
verbose_name=_("Status"),
|
||||
status=True,
|
||||
status_choices=STATUS_CHOICES)
|
||||
volume_name = BackupVolumeNameColumn("name",
|
||||
verbose_name=_("Volume Name"),
|
||||
link="horizon:project"
|
||||
":volumes:volumes:detail")
|
||||
|
||||
class Meta:
|
||||
name = "volume_backups"
|
||||
verbose_name = _("Volume Backups")
|
||||
status_columns = ("status",)
|
||||
row_class = UpdateRow
|
||||
table_actions = (DeleteBackup,)
|
||||
row_actions = (RestoreBackup, DeleteBackup)
|
@ -0,0 +1,44 @@
|
||||
# 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 exceptions
|
||||
from horizon import tabs
|
||||
|
||||
from openstack_dashboard.api import cinder
|
||||
|
||||
|
||||
class BackupOverviewTab(tabs.Tab):
|
||||
name = _("Overview")
|
||||
slug = "overview"
|
||||
template_name = ("project/volumes/backups/"
|
||||
"_detail_overview.html")
|
||||
|
||||
def get_context_data(self, request):
|
||||
try:
|
||||
backup = self.tab_group.kwargs['backup']
|
||||
volume = cinder.volume_get(request, backup.volume_id)
|
||||
return {'backup': backup,
|
||||
'volume': volume}
|
||||
except Exception:
|
||||
redirect = reverse('horizon:project:volumes:index')
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve backup details.'),
|
||||
redirect=redirect)
|
||||
|
||||
|
||||
class BackupDetailTabs(tabs.TabGroup):
|
||||
slug = "backup_details"
|
||||
tabs = (BackupOverviewTab,)
|
186
openstack_dashboard/dashboards/project/volumes/backups/tests.py
Normal file
186
openstack_dashboard/dashboards/project/volumes/backups/tests.py
Normal file
@ -0,0 +1,186 @@
|
||||
# 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 django.utils.http import urlencode
|
||||
from mox import IsA # noqa
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.test import helpers as test
|
||||
from openstack_dashboard.usage import quotas
|
||||
|
||||
|
||||
INDEX_URL = reverse('horizon:project:volumes:index')
|
||||
VOLUME_BACKUPS_TAB_URL = reverse('horizon:project:volumes:backups_tab')
|
||||
|
||||
|
||||
class VolumeBackupsViewTests(test.TestCase):
|
||||
|
||||
@test.create_stubs({api.cinder: ('volume_backup_create',)})
|
||||
def test_create_backup_post(self):
|
||||
volume = self.volumes.first()
|
||||
backup = self.cinder_volume_backups.first()
|
||||
|
||||
api.cinder.volume_backup_create(IsA(http.HttpRequest),
|
||||
volume.id,
|
||||
backup.container_name,
|
||||
backup.name,
|
||||
backup.description) \
|
||||
.AndReturn(backup)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
formData = {'method': 'CreateBackupForm',
|
||||
'tenant_id': self.tenant.id,
|
||||
'volume_id': volume.id,
|
||||
'container_name': backup.container_name,
|
||||
'name': backup.name,
|
||||
'description': backup.description}
|
||||
url = reverse('horizon:project:volumes:volumes:create_backup',
|
||||
args=[volume.id])
|
||||
res = self.client.post(url, formData)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertMessageCount(error=0, warning=0)
|
||||
self.assertRedirectsNoFollow(res, VOLUME_BACKUPS_TAB_URL)
|
||||
|
||||
@test.create_stubs({api.nova: ('server_list',),
|
||||
api.cinder: ('volume_snapshot_list',
|
||||
'volume_list',
|
||||
'volume_backup_supported',
|
||||
'volume_backup_list',
|
||||
'volume_backup_delete'),
|
||||
quotas: ('tenant_quota_usages',)})
|
||||
def test_delete_volume_backup(self):
|
||||
vol_backups = self.cinder_volume_backups.list()
|
||||
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(IsA(http.HttpRequest)). \
|
||||
AndReturn(vol_backups)
|
||||
api.cinder.volume_list(IsA(http.HttpRequest)). \
|
||||
AndReturn(volumes)
|
||||
api.cinder.volume_backup_delete(IsA(http.HttpRequest), backup.id)
|
||||
|
||||
api.cinder.volume_list(IsA(http.HttpRequest), search_opts=None). \
|
||||
AndReturn(volumes)
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None). \
|
||||
AndReturn([self.servers.list(), False])
|
||||
api.cinder.volume_snapshot_list(IsA(http.HttpRequest)). \
|
||||
AndReturn([])
|
||||
api.cinder.volume_list(IsA(http.HttpRequest)). \
|
||||
AndReturn(volumes)
|
||||
api.cinder.volume_backup_list(IsA(http.HttpRequest)). \
|
||||
AndReturn(vol_backups)
|
||||
api.cinder.volume_list(IsA(http.HttpRequest)). \
|
||||
AndReturn(volumes)
|
||||
quotas.tenant_quota_usages(IsA(http.HttpRequest)).MultipleTimes(). \
|
||||
AndReturn(self.quota_usages.first())
|
||||
self.mox.ReplayAll()
|
||||
|
||||
formData = {'action':
|
||||
'volume_backups__delete__%s' % backup.id}
|
||||
res = self.client.post(INDEX_URL +
|
||||
"?tab=volumes_and_snapshots__backups_tab",
|
||||
formData, follow=True)
|
||||
|
||||
self.assertIn("Scheduled deletion of Volume Backup: backup1",
|
||||
[m.message for m in res.context['messages']])
|
||||
|
||||
@test.create_stubs({api.cinder: ('volume_backup_get', 'volume_get')})
|
||||
def test_volume_backup_detail_get(self):
|
||||
backup = self.cinder_volume_backups.first()
|
||||
volume = self.cinder_volumes.get(id=backup.volume_id)
|
||||
|
||||
api.cinder.volume_backup_get(IsA(http.HttpRequest), backup.id). \
|
||||
AndReturn(backup)
|
||||
api.cinder.volume_get(IsA(http.HttpRequest), backup.volume_id). \
|
||||
AndReturn(volume)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:backups:detail',
|
||||
args=[backup.id])
|
||||
res = self.client.get(url)
|
||||
|
||||
self.assertContains(res,
|
||||
"<h2>Volume Backup Details: %s</h2>" %
|
||||
backup.name,
|
||||
1, 200)
|
||||
self.assertContains(res, "<dd>%s</dd>" % backup.name, 1, 200)
|
||||
self.assertContains(res, "<dd>%s</dd>" % backup.id, 1, 200)
|
||||
self.assertContains(res, "<dd>Available</dd>", 1, 200)
|
||||
|
||||
@test.create_stubs({api.cinder: ('volume_backup_get',)})
|
||||
def test_volume_backup_detail_get_with_exception(self):
|
||||
# Test to verify redirect if get volume backup fails
|
||||
backup = self.cinder_volume_backups.first()
|
||||
|
||||
api.cinder.volume_backup_get(IsA(http.HttpRequest), backup.id).\
|
||||
AndRaise(self.exceptions.cinder)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:backups:detail',
|
||||
args=[backup.id])
|
||||
res = self.client.get(url)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertMessageCount(error=1)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
@test.create_stubs({api.cinder: ('volume_backup_get', 'volume_get')})
|
||||
def test_volume_backup_detail_with_volume_get_exception(self):
|
||||
# Test to verify redirect if get volume fails
|
||||
backup = self.cinder_volume_backups.first()
|
||||
|
||||
api.cinder.volume_backup_get(IsA(http.HttpRequest), backup.id). \
|
||||
AndReturn(backup)
|
||||
api.cinder.volume_get(IsA(http.HttpRequest), backup.volume_id). \
|
||||
AndRaise(self.exceptions.cinder)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:backups:detail',
|
||||
args=[backup.id])
|
||||
res = self.client.get(url)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertMessageCount(error=1)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
@test.create_stubs({api.cinder: ('volume_list',
|
||||
'volume_backup_restore',)})
|
||||
def test_restore_backup(self):
|
||||
backup = self.cinder_volume_backups.first()
|
||||
volumes = self.cinder_volumes.list()
|
||||
|
||||
api.cinder.volume_list(IsA(http.HttpRequest)). \
|
||||
AndReturn(volumes)
|
||||
api.cinder.volume_backup_restore(IsA(http.HttpRequest),
|
||||
backup.id,
|
||||
backup.volume_id). \
|
||||
AndReturn(backup)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
formData = {'method': 'RestoreBackupForm',
|
||||
'backup_id': backup.id,
|
||||
'backup_name': backup.name,
|
||||
'volume_id': backup.volume_id}
|
||||
url = reverse('horizon:project:volumes:backups:restore',
|
||||
args=[backup.id])
|
||||
url += '?%s' % urlencode({'backup_name': backup.name,
|
||||
'volume_id': backup.volume_id})
|
||||
res = self.client.post(url, formData)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertMessageCount(success=1)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
@ -0,0 +1,30 @@
|
||||
# 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.project.volumes.backups import views
|
||||
|
||||
|
||||
VIEWS_MOD = ('openstack_dashboard.dashboards.project'
|
||||
'.volumes.backups.views')
|
||||
|
||||
|
||||
urlpatterns = patterns(VIEWS_MOD,
|
||||
url(r'^(?P<backup_id>[^/]+)/$',
|
||||
views.BackupDetailView.as_view(),
|
||||
name='detail'),
|
||||
url(r'^(?P<backup_id>[^/]+)/restore/$',
|
||||
views.RestoreBackupView.as_view(),
|
||||
name='restore'),
|
||||
)
|
@ -0,0 +1,88 @@
|
||||
# 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 import tabs
|
||||
from horizon.utils import memoized
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.dashboards.project.volumes.backups \
|
||||
import forms as backup_forms
|
||||
from openstack_dashboard.dashboards.project.volumes.backups \
|
||||
import tabs as backup_tabs
|
||||
|
||||
|
||||
class CreateBackupView(forms.ModalFormView):
|
||||
form_class = backup_forms.CreateBackupForm
|
||||
template_name = 'project/volumes/backups/create_backup.html'
|
||||
success_url = reverse_lazy("horizon:project:volumes:backups_tab")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(CreateBackupView, self).get_context_data(**kwargs)
|
||||
context['volume_id'] = self.kwargs['volume_id']
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
return {"volume_id": self.kwargs["volume_id"]}
|
||||
|
||||
|
||||
class BackupDetailView(tabs.TabView):
|
||||
tab_group_class = backup_tabs.BackupDetailTabs
|
||||
template_name = 'project/volumes/backups/detail.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(BackupDetailView, self).get_context_data(**kwargs)
|
||||
context["backup"] = self.get_data()
|
||||
return context
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_data(self):
|
||||
try:
|
||||
backup_id = self.kwargs['backup_id']
|
||||
backup = api.cinder.volume_backup_get(self.request,
|
||||
backup_id)
|
||||
except Exception:
|
||||
redirect = reverse('horizon:project:volumes:index')
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve backup details.'),
|
||||
redirect=redirect)
|
||||
return backup
|
||||
|
||||
def get_tabs(self, request, *args, **kwargs):
|
||||
backup = self.get_data()
|
||||
return self.tab_group_class(request, backup=backup, **kwargs)
|
||||
|
||||
|
||||
class RestoreBackupView(forms.ModalFormView):
|
||||
form_class = backup_forms.RestoreBackupForm
|
||||
template_name = 'project/volumes/backups/restore_backup.html'
|
||||
success_url = reverse_lazy('horizon:project:volumes:index')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(RestoreBackupView, self).get_context_data(**kwargs)
|
||||
context['backup_id'] = self.kwargs['backup_id']
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
backup_id = self.kwargs['backup_id']
|
||||
backup_name = self.request.GET.get('backup_name')
|
||||
volume_id = self.request.GET.get('volume_id')
|
||||
return {
|
||||
'backup_id': backup_id,
|
||||
'backup_name': backup_name,
|
||||
'volume_id': volume_id,
|
||||
}
|
@ -107,13 +107,18 @@ class VolumeSnapshotsViewTests(test.TestCase):
|
||||
@test.create_stubs({api.nova: ('server_list',),
|
||||
api.cinder: ('volume_snapshot_list',
|
||||
'volume_list',
|
||||
'volume_backup_supported',
|
||||
'volume_backup_list',
|
||||
'volume_snapshot_delete'),
|
||||
quotas: ('tenant_quota_usages',)})
|
||||
def test_delete_volume_snapshot(self):
|
||||
vol_snapshots = self.cinder_volume_snapshots.list()
|
||||
volumes = self.cinder_volumes.list()
|
||||
vol_backups = self.cinder_volume_backups.list()
|
||||
snapshot = self.cinder_volume_snapshots.first()
|
||||
|
||||
api.cinder.volume_backup_supported(IsA(http.HttpRequest)). \
|
||||
MultipleTimes().AndReturn(True)
|
||||
api.cinder.volume_snapshot_list(IsA(http.HttpRequest)). \
|
||||
AndReturn(vol_snapshots)
|
||||
api.cinder.volume_list(IsA(http.HttpRequest)). \
|
||||
@ -128,6 +133,10 @@ class VolumeSnapshotsViewTests(test.TestCase):
|
||||
AndReturn([])
|
||||
api.cinder.volume_list(IsA(http.HttpRequest)). \
|
||||
AndReturn(volumes)
|
||||
api.cinder.volume_backup_list(IsA(http.HttpRequest)). \
|
||||
AndReturn(vol_backups)
|
||||
api.cinder.volume_list(IsA(http.HttpRequest)). \
|
||||
AndReturn(volumes)
|
||||
quotas.tenant_quota_usages(IsA(http.HttpRequest)).MultipleTimes(). \
|
||||
AndReturn(self.quota_usages.first())
|
||||
self.mox.ReplayAll()
|
||||
|
@ -20,6 +20,8 @@ from horizon import tabs
|
||||
|
||||
from openstack_dashboard import api
|
||||
|
||||
from openstack_dashboard.dashboards.project.volumes.backups \
|
||||
import tables as backups_tables
|
||||
from openstack_dashboard.dashboards.project.volumes.snapshots \
|
||||
import tables as vol_snapshot_tables
|
||||
from openstack_dashboard.dashboards.project.volumes.volumes \
|
||||
@ -95,7 +97,30 @@ class SnapshotTab(tabs.TableTab):
|
||||
return snapshots
|
||||
|
||||
|
||||
class BackupsTab(tabs.TableTab, VolumeTableMixIn):
|
||||
table_classes = (backups_tables.BackupsTable,)
|
||||
name = _("Volume Backups")
|
||||
slug = "backups_tab"
|
||||
template_name = ("horizon/common/_detail_table.html")
|
||||
|
||||
def allowed(self, request):
|
||||
return api.cinder.volume_backup_supported(self.request)
|
||||
|
||||
def get_volume_backups_data(self):
|
||||
try:
|
||||
backups = api.cinder.volume_backup_list(self.request)
|
||||
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 VolumeAndSnapshotTabs(tabs.TabGroup):
|
||||
slug = "volumes_and_snapshots"
|
||||
tabs = (VolumeTab, SnapshotTab,)
|
||||
tabs = (VolumeTab, SnapshotTab, BackupsTab)
|
||||
sticky = True
|
||||
|
@ -0,0 +1,26 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}{% endblock %}
|
||||
{% block form_action %}{% url 'horizon:project:volumes:volumes:create_backup' volume_id %}{% endblock %}
|
||||
|
||||
{% block modal_id %}create_volume_backup_modal{% endblock %}
|
||||
{% block modal-header %}{% trans "Create Volume Backup" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="left">
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="right">
|
||||
<p><strong>{% trans "Volume Backup" %}</strong>: {% trans "Volume Backups are stored using the Object Storage service. You must have this service activated in order to create a backup." %}</p>
|
||||
<p>{% trans "If no container name is provided, a default container named volumebackups will be provisioned for you. Backups will be the same size as the volume they originate from." %}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Volume Backup" %}" />
|
||||
<a href="{% url 'horizon:project:volumes:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
@ -0,0 +1,51 @@
|
||||
{% load i18n sizeformat parse_date %}
|
||||
{% load url from future %}
|
||||
|
||||
<h3>{% trans "Volume Backup Overview" %}: {{backup.display_name }}</h3>
|
||||
|
||||
<div class="info row-fluid detail">
|
||||
<h4>{% trans "Info" %}</h4>
|
||||
<hr class="header_rule">
|
||||
<dl>
|
||||
<dt>{% trans "Name" %}</dt>
|
||||
<dd>{{ backup.name }}</dd>
|
||||
<dt>{% trans "ID" %}</dt>
|
||||
<dd>{{ backup.id }}</dd>
|
||||
{% if backup.description %}
|
||||
<dt>{% trans "Description" %}</dt>
|
||||
<dd>{{ backup.description }}</dd>
|
||||
{% endif %}
|
||||
<dt>{% trans "Status" %}</dt>
|
||||
<dd>{{ backup.status|capfirst }}</dd>
|
||||
<dt>{% trans "Volume" %}</dt>
|
||||
<dd>
|
||||
<a href="{% url 'horizon:project:volumes:volumes:detail' backup.volume_id %}">
|
||||
{{ volume.name }}
|
||||
</a>
|
||||
</dd>
|
||||
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="specs row-fluid detail">
|
||||
<h4>{% trans "Specs" %}</h4>
|
||||
<hr class="header_rule">
|
||||
<dl>
|
||||
<dt>{% trans "Size" %}</dt>
|
||||
<dd>{{ backup.size }} {% trans "GB" %}</dd>
|
||||
<dt>{% trans "Created" %}</dt>
|
||||
<dd>{{ backup.created_at|parse_date }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="status row-fluid detail">
|
||||
<h4>{% trans "Metadata" %}</h4>
|
||||
<hr class="header_rule">
|
||||
<dl>
|
||||
{% for key, value in backup.metadata.items %}
|
||||
<dt>{{ key }}</dt>
|
||||
<dd>{{ value }}</dd>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
</div>
|
@ -0,0 +1,26 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}{% endblock %}
|
||||
{% block form_action %}{% url 'horizon:project:volumes:backups:restore' backup_id %}{% endblock %}
|
||||
|
||||
{% block modal_id %}restore_volume_backup_modal{% endblock %}
|
||||
{% block modal-header %}{% trans "Restore Volume Backup" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="left">
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="right">
|
||||
<p><strong>{% trans "Restore Backup" %}</strong>: {% trans "Select a volume to restore to." %}</p>
|
||||
<p>{% trans "Optionally, you may choose to create a new volume." %}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Restore Backup to Volume"%}" />
|
||||
<a href="{% url 'horizon:project:volumes:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Create Volume Backup" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Create a Volume Backup") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/volumes/backups/_create_backup.html' %}
|
||||
{% endblock %}
|
@ -0,0 +1,14 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Volume Backup Details" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Volume Backup Details: ")|add:backup.name|default:_("Volume Backup Details:") %}
|
||||
{% endblock page_header %}
|
||||
{% block main %}
|
||||
<div class="row-fluid">
|
||||
<div class="span12">
|
||||
{{ tab_group.render }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Restore Volume Backup" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Restore a Volume Backup") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/volumes/backups/_restore_backup.html' %}
|
||||
{% endblock %}
|
@ -1,9 +1,9 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Volumes & Snapshots" %}{% endblock %}
|
||||
{% block title %}{% trans "Volumes" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Volumes & Snapshots")%}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Volumes")%}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
|
@ -27,13 +27,19 @@ INDEX_URL = reverse('horizon:project:volumes:index')
|
||||
|
||||
class VolumeAndSnapshotsTests(test.TestCase):
|
||||
@test.create_stubs({api.cinder: ('volume_list',
|
||||
'volume_snapshot_list',),
|
||||
'volume_snapshot_list',
|
||||
'volume_backup_supported',
|
||||
'volume_backup_list',
|
||||
),
|
||||
api.nova: ('server_list',),
|
||||
quotas: ('tenant_quota_usages',)})
|
||||
def test_index(self):
|
||||
def _test_index(self, backup_supported=True):
|
||||
vol_backups = self.cinder_volume_backups.list()
|
||||
vol_snaps = self.cinder_volume_snapshots.list()
|
||||
volumes = self.cinder_volumes.list()
|
||||
|
||||
api.cinder.volume_backup_supported(IsA(http.HttpRequest)).\
|
||||
MultipleTimes().AndReturn(backup_supported)
|
||||
api.cinder.volume_list(IsA(http.HttpRequest), search_opts=None).\
|
||||
AndReturn(volumes)
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None).\
|
||||
@ -41,6 +47,10 @@ class VolumeAndSnapshotsTests(test.TestCase):
|
||||
api.cinder.volume_snapshot_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(vol_snaps)
|
||||
api.cinder.volume_list(IsA(http.HttpRequest)).AndReturn(volumes)
|
||||
if backup_supported:
|
||||
api.cinder.volume_backup_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(vol_backups)
|
||||
api.cinder.volume_list(IsA(http.HttpRequest)).AndReturn(volumes)
|
||||
quotas.tenant_quota_usages(IsA(http.HttpRequest)).MultipleTimes(). \
|
||||
AndReturn(self.quota_usages.first())
|
||||
self.mox.ReplayAll()
|
||||
@ -48,3 +58,9 @@ class VolumeAndSnapshotsTests(test.TestCase):
|
||||
res = self.client.get(INDEX_URL)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertTemplateUsed(res, 'project/volumes/index.html')
|
||||
|
||||
def test_index_back_supported(self):
|
||||
self._test_index(backup_supported=True)
|
||||
|
||||
def test_index_backup_not_supported(self):
|
||||
self._test_index(backup_supported=False)
|
||||
|
@ -16,19 +16,23 @@ 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.project.volumes.backups \
|
||||
import urls as backups_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.volumes \
|
||||
import urls as volume_urls
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
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$',
|
||||
views.IndexView.as_view(), name='volumes_tab'),
|
||||
url(r'^\?tab=volumes_and_snapshots__backups_tab$',
|
||||
views.IndexView.as_view(), name='backups_tab'),
|
||||
url(r'', include(volume_urls, namespace='volumes')),
|
||||
url(r'backups/', include(backups_urls, namespace='backups')),
|
||||
url(r'snapshots/', include(snapshot_urls, namespace='snapshots')),
|
||||
)
|
||||
|
@ -162,6 +162,24 @@ class CreateSnapshot(tables.LinkAction):
|
||||
return volume.status in ("available", "in-use")
|
||||
|
||||
|
||||
class CreateBackup(tables.LinkAction):
|
||||
name = "backups"
|
||||
verbose_name = _("Create Backup")
|
||||
url = "horizon:project:volumes:volumes:create_backup"
|
||||
classes = ("ajax-modal",)
|
||||
policy_rules = (("volume", "backup:create"),)
|
||||
|
||||
def get_policy_target(self, request, datum=None):
|
||||
project_id = None
|
||||
if datum:
|
||||
project_id = getattr(datum, "os-vol-tenant-attr:tenant_id", None)
|
||||
return {"project_id": project_id}
|
||||
|
||||
def allowed(self, request, volume=None):
|
||||
return (cinder.volume_backup_supported(request) and
|
||||
volume.status == "available")
|
||||
|
||||
|
||||
class EditVolume(tables.LinkAction):
|
||||
name = "edit"
|
||||
verbose_name = _("Edit Volume")
|
||||
@ -298,7 +316,7 @@ class VolumesTable(VolumesTableBase):
|
||||
row_class = UpdateRow
|
||||
table_actions = (CreateVolume, DeleteVolume, VolumesFilterAction)
|
||||
row_actions = (EditVolume, ExtendVolume, LaunchVolume, EditAttachments,
|
||||
CreateSnapshot, DeleteVolume)
|
||||
CreateSnapshot, CreateBackup, DeleteVolume)
|
||||
|
||||
|
||||
class DetachVolume(tables.BatchAction):
|
||||
|
@ -706,6 +706,8 @@ class VolumeViewTests(test.TestCase):
|
||||
|
||||
@test.create_stubs({cinder: ('volume_list',
|
||||
'volume_snapshot_list',
|
||||
'volume_backup_supported',
|
||||
'volume_backup_list',
|
||||
'volume_delete',),
|
||||
api.nova: ('server_list',),
|
||||
quotas: ('tenant_quota_usages',)})
|
||||
@ -715,6 +717,8 @@ class VolumeViewTests(test.TestCase):
|
||||
formData = {'action':
|
||||
'volumes__delete__%s' % volume.id}
|
||||
|
||||
cinder.volume_backup_supported(IsA(http.HttpRequest)). \
|
||||
MultipleTimes().AndReturn(True)
|
||||
cinder.volume_list(IsA(http.HttpRequest), search_opts=None).\
|
||||
AndReturn(volumes)
|
||||
cinder.volume_delete(IsA(http.HttpRequest), volume.id)
|
||||
@ -724,6 +728,10 @@ class VolumeViewTests(test.TestCase):
|
||||
AndReturn(self.cinder_volume_snapshots.list())
|
||||
cinder.volume_list(IsA(http.HttpRequest), search_opts=None).\
|
||||
AndReturn(volumes)
|
||||
cinder.volume_backup_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.cinder_volume_backups.list())
|
||||
cinder.volume_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(volumes)
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None).\
|
||||
AndReturn([self.servers.list(), False])
|
||||
cinder.volume_list(IsA(http.HttpRequest)).AndReturn(volumes)
|
||||
@ -739,6 +747,8 @@ class VolumeViewTests(test.TestCase):
|
||||
|
||||
@test.create_stubs({cinder: ('volume_list',
|
||||
'volume_snapshot_list',
|
||||
'volume_backup_supported',
|
||||
'volume_backup_list',
|
||||
'volume_delete',),
|
||||
api.nova: ('server_list',),
|
||||
quotas: ('tenant_quota_usages',)})
|
||||
@ -750,6 +760,8 @@ class VolumeViewTests(test.TestCase):
|
||||
exc = self.exceptions.cinder.__class__(400,
|
||||
"error: dependent snapshots")
|
||||
|
||||
cinder.volume_backup_supported(IsA(http.HttpRequest)). \
|
||||
MultipleTimes().AndReturn(True)
|
||||
cinder.volume_list(IsA(http.HttpRequest), search_opts=None).\
|
||||
AndReturn(volumes)
|
||||
cinder.volume_delete(IsA(http.HttpRequest), volume.id).\
|
||||
@ -763,6 +775,10 @@ class VolumeViewTests(test.TestCase):
|
||||
cinder.volume_snapshot_list(IsA(http.HttpRequest))\
|
||||
.AndReturn(self.cinder_volume_snapshots.list())
|
||||
cinder.volume_list(IsA(http.HttpRequest)).AndReturn(volumes)
|
||||
cinder.volume_backup_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.cinder_volume_backups.list())
|
||||
cinder.volume_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(volumes)
|
||||
quotas.tenant_quota_usages(IsA(http.HttpRequest)).MultipleTimes().\
|
||||
AndReturn(self.quota_usages.first())
|
||||
|
||||
@ -857,7 +873,9 @@ class VolumeViewTests(test.TestCase):
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_list',
|
||||
'volume_snapshot_list'),
|
||||
'volume_snapshot_list',
|
||||
'volume_backup_supported',
|
||||
'volume_backup_list',),
|
||||
api.nova: ('server_list',),
|
||||
quotas: ('tenant_quota_usages',)})
|
||||
def test_create_button_disabled_when_quota_exceeded(self):
|
||||
@ -865,6 +883,8 @@ class VolumeViewTests(test.TestCase):
|
||||
quota_usages['volumes']['available'] = 0
|
||||
volumes = self.cinder_volumes.list()
|
||||
|
||||
api.cinder.volume_backup_supported(IsA(http.HttpRequest)). \
|
||||
MultipleTimes().AndReturn(True)
|
||||
cinder.volume_list(IsA(http.HttpRequest), search_opts=None)\
|
||||
.AndReturn(volumes)
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None)\
|
||||
@ -872,6 +892,9 @@ class VolumeViewTests(test.TestCase):
|
||||
cinder.volume_snapshot_list(IsA(http.HttpRequest))\
|
||||
.AndReturn(self.cinder_volume_snapshots.list())
|
||||
cinder.volume_list(IsA(http.HttpRequest)).AndReturn(volumes)
|
||||
cinder.volume_backup_list(IsA(http.HttpRequest))\
|
||||
.AndReturn(self.cinder_volume_backups.list())
|
||||
cinder.volume_list(IsA(http.HttpRequest)).AndReturn(volumes)
|
||||
quotas.tenant_quota_usages(IsA(http.HttpRequest))\
|
||||
.MultipleTimes().AndReturn(quota_usages)
|
||||
|
||||
|
@ -17,6 +17,8 @@ from django.conf.urls import url # noqa
|
||||
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
.volumes import views
|
||||
from openstack_dashboard.dashboards.project.volumes.backups \
|
||||
import views as backup_views
|
||||
|
||||
|
||||
VIEWS_MOD = ('openstack_dashboard.dashboards.project.volumes.volumes.views')
|
||||
@ -32,6 +34,9 @@ urlpatterns = patterns(VIEWS_MOD,
|
||||
url(r'^(?P<volume_id>[^/]+)/create_snapshot/$',
|
||||
views.CreateSnapshotView.as_view(),
|
||||
name='create_snapshot'),
|
||||
url(r'^(?P<volume_id>[^/]+)/create_backup/$',
|
||||
backup_views.CreateBackupView.as_view(),
|
||||
name='create_backup'),
|
||||
url(r'^(?P<volume_id>[^/]+)/$',
|
||||
views.DetailView.as_view(),
|
||||
name='detail'),
|
||||
|
@ -168,6 +168,12 @@ OPENSTACK_HYPERVISOR_FEATURES = {
|
||||
'can_set_password': False,
|
||||
}
|
||||
|
||||
# The OPENSTACK_CINDER_FEATURES settings can be used to enable optional
|
||||
# services provided by cinder that is not exposed by its extension API.
|
||||
OPENSTACK_CINDER_FEATURES = {
|
||||
'enable_backup': False,
|
||||
}
|
||||
|
||||
# The OPENSTACK_NEUTRON_NETWORK settings can be used to enable optional
|
||||
# services provided by neutron. Options currently available are load
|
||||
# balancer service, security groups, quotas, VPN service.
|
||||
|
@ -112,6 +112,10 @@ OPENSTACK_KEYSTONE_BACKEND = {
|
||||
'can_edit_role': True
|
||||
}
|
||||
|
||||
OPENSTACK_CINDER_FEATURES = {
|
||||
'enable_backup': True,
|
||||
}
|
||||
|
||||
OPENSTACK_NEUTRON_NETWORK = {
|
||||
'enable_lb': True,
|
||||
'enable_firewall': True,
|
||||
|
@ -17,6 +17,7 @@ from cinderclient.v1 import quotas
|
||||
from cinderclient.v1 import services
|
||||
from cinderclient.v1 import volume_snapshots as vol_snaps
|
||||
from cinderclient.v1 import volumes
|
||||
from cinderclient.v2 import volume_backups as vol_backups
|
||||
from cinderclient.v2 import volume_snapshots as vol_snaps_v2
|
||||
from cinderclient.v2 import volumes as volumes_v2
|
||||
|
||||
@ -29,12 +30,13 @@ from openstack_dashboard.test.test_data import utils
|
||||
def data(TEST):
|
||||
TEST.cinder_services = utils.TestDataContainer()
|
||||
TEST.cinder_volumes = utils.TestDataContainer()
|
||||
TEST.cinder_volume_backups = utils.TestDataContainer()
|
||||
TEST.cinder_volume_snapshots = utils.TestDataContainer()
|
||||
TEST.cinder_quotas = utils.TestDataContainer()
|
||||
TEST.cinder_quota_usages = utils.TestDataContainer()
|
||||
TEST.cinder_availability_zones = utils.TestDataContainer()
|
||||
|
||||
# Services
|
||||
# Services
|
||||
service_1 = services.Service(services.ServiceManager(None),
|
||||
{
|
||||
"service": "cinder-scheduler",
|
||||
@ -138,6 +140,29 @@ def data(TEST):
|
||||
TEST.cinder_volume_snapshots.add(api.cinder.VolumeSnapshot(snapshot))
|
||||
TEST.cinder_volume_snapshots.add(api.cinder.VolumeSnapshot(snapshot2))
|
||||
|
||||
volume_backup1 = vol_backups.VolumeBackup(vol_backups.
|
||||
VolumeBackupManager(None),
|
||||
{'id': 'a374cbb8-3f99-4c3f-a2ef-3edbec842e31',
|
||||
'name': 'backup1',
|
||||
'description': 'volume backup 1',
|
||||
'size': 10,
|
||||
'status': 'available',
|
||||
'container_name': 'volumebackups',
|
||||
'volume_id': '11023e92-8008-4c8b-8059-7f2293ff3887'})
|
||||
|
||||
volume_backup2 = vol_backups.VolumeBackup(vol_backups.
|
||||
VolumeBackupManager(None),
|
||||
{'id': 'c321cbb8-3f99-4c3f-a2ef-3edbec842e52',
|
||||
'name': 'backup2',
|
||||
'description': 'volume backup 2',
|
||||
'size': 20,
|
||||
'status': 'available',
|
||||
'container_name': 'volumebackups',
|
||||
'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'})
|
||||
|
||||
TEST.cinder_volume_backups.add(volume_backup1)
|
||||
TEST.cinder_volume_backups.add(volume_backup2)
|
||||
|
||||
# Quota Sets
|
||||
quota_data = dict(volumes='1',
|
||||
snapshots='1',
|
||||
|
Loading…
x
Reference in New Issue
Block a user