summaryrefslogtreecommitdiff
path: root/openstack_dashboard/dashboards
diff options
context:
space:
mode:
authorLaura Frank <ljfrank@gmail.com>2014-03-28 10:50:01 -0500
committerlin-hua-cheng <lin-hua.cheng@hp.com>2014-07-04 03:07:53 -0700
commit630bf3d5a4b6415dcfe5c47e2abc27b21f364b71 (patch)
treef0d8a4c060f66650ab8c41300b10aeb204e0d653 /openstack_dashboard/dashboards
parent67892d789d1a74cfebbba94fd3c4e42ba7858553 (diff)
downloadhorizon-630bf3d5a4b6415dcfe5c47e2abc27b21f364b71.tar.gz
Adding support for volume backups
Users can create, view, delete and restore volume backups Change-Id: I85b372013c4573018d39178314e769ec8adfd3c7 Co-Authored-By: Lin Hua Cheng <lin-hua.cheng@hp.com> Implements: blueprint volume-backups
Diffstat (limited to 'openstack_dashboard/dashboards')
-rw-r--r--openstack_dashboard/dashboards/project/volumes/backups/__init__.py0
-rw-r--r--openstack_dashboard/dashboards/project/volumes/backups/forms.py109
-rw-r--r--openstack_dashboard/dashboards/project/volumes/backups/tables.py130
-rw-r--r--openstack_dashboard/dashboards/project/volumes/backups/tabs.py44
-rw-r--r--openstack_dashboard/dashboards/project/volumes/backups/tests.py186
-rw-r--r--openstack_dashboard/dashboards/project/volumes/backups/urls.py30
-rw-r--r--openstack_dashboard/dashboards/project/volumes/backups/views.py88
-rw-r--r--openstack_dashboard/dashboards/project/volumes/snapshots/tests.py9
-rw-r--r--openstack_dashboard/dashboards/project/volumes/tabs.py27
-rw-r--r--openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/_create_backup.html26
-rw-r--r--openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/_detail_overview.html51
-rw-r--r--openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/_restore_backup.html26
-rw-r--r--openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/create_backup.html11
-rw-r--r--openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/detail.html14
-rw-r--r--openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/restore_backup.html11
-rw-r--r--openstack_dashboard/dashboards/project/volumes/templates/volumes/index.html4
-rw-r--r--openstack_dashboard/dashboards/project/volumes/test.py20
-rw-r--r--openstack_dashboard/dashboards/project/volumes/urls.py6
-rw-r--r--openstack_dashboard/dashboards/project/volumes/volumes/tables.py20
-rw-r--r--openstack_dashboard/dashboards/project/volumes/volumes/tests.py25
-rw-r--r--openstack_dashboard/dashboards/project/volumes/volumes/urls.py5
21 files changed, 834 insertions, 8 deletions
diff --git a/openstack_dashboard/dashboards/project/volumes/backups/__init__.py b/openstack_dashboard/dashboards/project/volumes/backups/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/volumes/backups/__init__.py
diff --git a/openstack_dashboard/dashboards/project/volumes/backups/forms.py b/openstack_dashboard/dashboards/project/volumes/backups/forms.py
new file mode 100644
index 000000000..885fbaa42
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/volumes/backups/forms.py
@@ -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
diff --git a/openstack_dashboard/dashboards/project/volumes/backups/tables.py b/openstack_dashboard/dashboards/project/volumes/backups/tables.py
new file mode 100644
index 000000000..c0c245588
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/volumes/backups/tables.py
@@ -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)
diff --git a/openstack_dashboard/dashboards/project/volumes/backups/tabs.py b/openstack_dashboard/dashboards/project/volumes/backups/tabs.py
new file mode 100644
index 000000000..87bcd4c44
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/volumes/backups/tabs.py
@@ -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,)
diff --git a/openstack_dashboard/dashboards/project/volumes/backups/tests.py b/openstack_dashboard/dashboards/project/volumes/backups/tests.py
new file mode 100644
index 000000000..8b22b140d
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/volumes/backups/tests.py
@@ -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)
diff --git a/openstack_dashboard/dashboards/project/volumes/backups/urls.py b/openstack_dashboard/dashboards/project/volumes/backups/urls.py
new file mode 100644
index 000000000..9d3d9ab92
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/volumes/backups/urls.py
@@ -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'),
+)
diff --git a/openstack_dashboard/dashboards/project/volumes/backups/views.py b/openstack_dashboard/dashboards/project/volumes/backups/views.py
new file mode 100644
index 000000000..a04fee850
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/volumes/backups/views.py
@@ -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,
+ }
diff --git a/openstack_dashboard/dashboards/project/volumes/snapshots/tests.py b/openstack_dashboard/dashboards/project/volumes/snapshots/tests.py
index 426b7ba9f..858d8fea1 100644
--- a/openstack_dashboard/dashboards/project/volumes/snapshots/tests.py
+++ b/openstack_dashboard/dashboards/project/volumes/snapshots/tests.py
@@ -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()
diff --git a/openstack_dashboard/dashboards/project/volumes/tabs.py b/openstack_dashboard/dashboards/project/volumes/tabs.py
index 3b9e426a3..e067c9f51 100644
--- a/openstack_dashboard/dashboards/project/volumes/tabs.py
+++ b/openstack_dashboard/dashboards/project/volumes/tabs.py
@@ -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
diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/_create_backup.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/_create_backup.html
new file mode 100644
index 000000000..21c0d34ef
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/_create_backup.html
@@ -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 %}
diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/_detail_overview.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/_detail_overview.html
new file mode 100644
index 000000000..8bb859763
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/_detail_overview.html
@@ -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>
diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/_restore_backup.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/_restore_backup.html
new file mode 100644
index 000000000..d0df6278c
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/_restore_backup.html
@@ -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 %}
diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/create_backup.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/create_backup.html
new file mode 100644
index 000000000..abdc3bc4e
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/create_backup.html
@@ -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 %}
diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/detail.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/detail.html
new file mode 100644
index 000000000..c195f0120
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/detail.html
@@ -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 %}
diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/restore_backup.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/restore_backup.html
new file mode 100644
index 000000000..ff053ea55
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/backups/restore_backup.html
@@ -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 %} \ No newline at end of file
diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/index.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/index.html
index 5df0fcaae..57333ed5e 100644
--- a/openstack_dashboard/dashboards/project/volumes/templates/volumes/index.html
+++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/index.html
@@ -1,9 +1,9 @@
{% extends 'base.html' %}
{% load i18n %}
-{% block title %}{% trans "Volumes &amp; Snapshots" %}{% endblock %}
+{% block title %}{% trans "Volumes" %}{% endblock %}
{% block page_header %}
- {% include "horizon/common/_page_header.html" with title=_("Volumes &amp; Snapshots")%}
+ {% include "horizon/common/_page_header.html" with title=_("Volumes")%}
{% endblock page_header %}
{% block main %}
diff --git a/openstack_dashboard/dashboards/project/volumes/test.py b/openstack_dashboard/dashboards/project/volumes/test.py
index 5a1694447..405461c79 100644
--- a/openstack_dashboard/dashboards/project/volumes/test.py
+++ b/openstack_dashboard/dashboards/project/volumes/test.py
@@ -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)
diff --git a/openstack_dashboard/dashboards/project/volumes/urls.py b/openstack_dashboard/dashboards/project/volumes/urls.py
index 4831f5fc0..c6e66a724 100644
--- a/openstack_dashboard/dashboards/project/volumes/urls.py
+++ b/openstack_dashboard/dashboards/project/volumes/urls.py
@@ -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')),
)
diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/tables.py b/openstack_dashboard/dashboards/project/volumes/volumes/tables.py
index 7568296ce..eaacbe8b0 100644
--- a/openstack_dashboard/dashboards/project/volumes/volumes/tables.py
+++ b/openstack_dashboard/dashboards/project/volumes/volumes/tables.py
@@ -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):
diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/tests.py b/openstack_dashboard/dashboards/project/volumes/volumes/tests.py
index cdee82127..f893a98c5 100644
--- a/openstack_dashboard/dashboards/project/volumes/volumes/tests.py
+++ b/openstack_dashboard/dashboards/project/volumes/volumes/tests.py
@@ -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)
diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/urls.py b/openstack_dashboard/dashboards/project/volumes/volumes/urls.py
index ced497b13..a2d8ce314 100644
--- a/openstack_dashboard/dashboards/project/volumes/volumes/urls.py
+++ b/openstack_dashboard/dashboards/project/volumes/volumes/urls.py
@@ -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'),