diff --git a/doc/source/topics/settings.rst b/doc/source/topics/settings.rst
index 1f853a86d7..ad9b508f1e 100644
--- a/doc/source/topics/settings.rst
+++ b/doc/source/topics/settings.rst
@@ -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``
-----------------------------
diff --git a/openstack_dashboard/api/cinder.py b/openstack_dashboard/api/cinder.py
index 089f4ddc48..bb54045d83 100644
--- a/openstack_dashboard/api/cinder.py
+++ b/openstack_dashboard/api/cinder.py
@@ -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:
diff --git a/openstack_dashboard/conf/cinder_policy.json b/openstack_dashboard/conf/cinder_policy.json
index eb32440619..7d1ef6317c 100644
--- a/openstack_dashboard/conf/cinder_policy.json
+++ b/openstack_dashboard/conf/cinder_policy.json
@@ -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": []
}
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 0000000000..e69de29bb2
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 0000000000..885fbaa420
--- /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 0000000000..c0c2455881
--- /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 0000000000..87bcd4c446
--- /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 0000000000..8b22b140d0
--- /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,
+ "
Volume Backup Details: %s " %
+ backup.name,
+ 1, 200)
+ self.assertContains(res, "%s " % backup.name, 1, 200)
+ self.assertContains(res, "%s " % backup.id, 1, 200)
+ self.assertContains(res, "Available ", 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 0000000000..9d3d9ab925
--- /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[^/]+)/$',
+ views.BackupDetailView.as_view(),
+ name='detail'),
+ url(r'^(?P[^/]+)/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 0000000000..a04fee8507
--- /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 426b7ba9f9..858d8fea11 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 3b9e426a36..e067c9f514 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 0000000000..21c0d34ef1
--- /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 %}
+
+
+ {% include "horizon/common/_form_fields.html" %}
+
+
+
+
{% trans "Volume Backup" %} : {% trans "Volume Backups are stored using the Object Storage service. You must have this service activated in order to create a backup." %}
+
{% 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." %}
+
+{% endblock %}
+
+{% block modal-footer %}
+
+ {% trans "Cancel" %}
+{% 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 0000000000..8bb8597637
--- /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 %}
+
+{% trans "Volume Backup Overview" %}: {{backup.display_name }}
+
+
+
{% trans "Info" %}
+
+
+ {% trans "Name" %}
+ {{ backup.name }}
+ {% trans "ID" %}
+ {{ backup.id }}
+ {% if backup.description %}
+ {% trans "Description" %}
+ {{ backup.description }}
+ {% endif %}
+ {% trans "Status" %}
+ {{ backup.status|capfirst }}
+ {% trans "Volume" %}
+
+
+ {{ volume.name }}
+
+
+
+
+
+
+
+
{% trans "Specs" %}
+
+
+ {% trans "Size" %}
+ {{ backup.size }} {% trans "GB" %}
+ {% trans "Created" %}
+ {{ backup.created_at|parse_date }}
+
+
+
+
+
+
{% trans "Metadata" %}
+
+
+ {% for key, value in backup.metadata.items %}
+ {{ key }}
+ {{ value }}
+ {% endfor %}
+
+
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 0000000000..d0df6278c5
--- /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 %}
+
+
+ {% include "horizon/common/_form_fields.html" %}
+
+
+
+
{% trans "Restore Backup" %} : {% trans "Select a volume to restore to." %}
+
{% trans "Optionally, you may choose to create a new volume." %}
+
+{% endblock %}
+
+{% block modal-footer %}
+
+ {% trans "Cancel" %}
+{% 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 0000000000..abdc3bc4ef
--- /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 0000000000..c195f01206
--- /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 %}
+
+
+ {{ tab_group.render }}
+
+
+{% 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 0000000000..ff053ea556
--- /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 5df0fcaae4..57333ed5e5 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 & 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 %}
diff --git a/openstack_dashboard/dashboards/project/volumes/test.py b/openstack_dashboard/dashboards/project/volumes/test.py
index 5a1694447e..405461c793 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 4831f5fc09..c6e66a724f 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 7568296cea..eaacbe8b0d 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 cdee821273..f893a98c5e 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 ced497b13b..a2d8ce314c 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[^/]+)/create_snapshot/$',
views.CreateSnapshotView.as_view(),
name='create_snapshot'),
+ url(r'^(?P[^/]+)/create_backup/$',
+ backup_views.CreateBackupView.as_view(),
+ name='create_backup'),
url(r'^(?P[^/]+)/$',
views.DetailView.as_view(),
name='detail'),
diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example
index 3a3a20e3ad..25a8ff082f 100644
--- a/openstack_dashboard/local/local_settings.py.example
+++ b/openstack_dashboard/local/local_settings.py.example
@@ -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.
diff --git a/openstack_dashboard/test/settings.py b/openstack_dashboard/test/settings.py
index 5e9b01b002..8c2cd00783 100644
--- a/openstack_dashboard/test/settings.py
+++ b/openstack_dashboard/test/settings.py
@@ -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,
diff --git a/openstack_dashboard/test/test_data/cinder_data.py b/openstack_dashboard/test/test_data/cinder_data.py
index 4c9233f86a..bfa1f609e9 100644
--- a/openstack_dashboard/test/test_data/cinder_data.py
+++ b/openstack_dashboard/test/test_data/cinder_data.py
@@ -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',