From 9fa1cddf0922af186bd102768416ec9c5b84f511 Mon Sep 17 00:00:00 2001
From: Jesper Schmitz Mouridsen
Date: Wed, 26 Jun 2019 18:22:58 +0000
Subject: [PATCH] Enable volume snapshot backups
Make it possible to use horizon for backing
up snapshots.
* Add snapshot column to backups table
* Add snapshot information to backup detail
* Fix wrong template path in create_backup.html
* Add ChoiceField with snapshots belonging
to the volume in create backup form
* Add create backup option in snapshots table
* Adjust tests and test data
When backing up from snapshots table the snapshot
ChoiceField is preset with the choosen snapshot
as the only option.
Implements: blueprint volume-snapshot-backups
Change-Id: I4b7707d95756501e0622460e3ddc4e3f7624f02e
---
openstack_dashboard/api/cinder.py | 16 +-
.../dashboards/project/backups/forms.py | 52 ++++++-
.../dashboards/project/backups/tables.py | 29 ++++
.../dashboards/project/backups/tabs.py | 13 +-
.../templates/backups/_create_backup.html | 5 +
.../templates/backups/_detail_overview.html | 14 ++
.../templates/backups/create_backup.html | 2 +-
.../dashboards/project/backups/tests.py | 143 ++++++++++++++++--
.../dashboards/project/backups/views.py | 6 +
.../dashboards/project/snapshots/tables.py | 21 ++-
.../dashboards/project/volumes/urls.py | 3 +
.../test/test_data/cinder_data.py | 7 +
...ume-snapshot-backups-54e4d18633fd4c5d.yaml | 5 +
13 files changed, 291 insertions(+), 25 deletions(-)
create mode 100644 releasenotes/notes/bp-volume-snapshot-backups-54e4d18633fd4c5d.yaml
diff --git a/openstack_dashboard/api/cinder.py b/openstack_dashboard/api/cinder.py
index 01583b42ae..1cd3c1db7b 100644
--- a/openstack_dashboard/api/cinder.py
+++ b/openstack_dashboard/api/cinder.py
@@ -123,9 +123,9 @@ class VolumeType(BaseCinderAPIResourceWrapper):
class VolumeBackup(BaseCinderAPIResourceWrapper):
_attrs = ['id', 'name', 'description', 'container', 'size', 'status',
- 'created_at', 'volume_id', 'availability_zone']
+ 'created_at', 'volume_id', 'availability_zone', 'snapshot_id']
_volume = None
-
+ _snapshot = None
@property
def volume(self):
return self._volume
@@ -134,6 +134,14 @@ class VolumeBackup(BaseCinderAPIResourceWrapper):
def volume(self, value):
self._volume = value
+ @property
+ def snapshot(self):
+ return self._snapshot
+
+ @snapshot.setter
+ def snapshot(self, value):
+ self._snapshot = value
+
class QosSpecs(BaseCinderAPIResourceWrapper):
@@ -630,7 +638,8 @@ def volume_backup_create(request,
container_name,
name,
description,
- force=False):
+ force=False,
+ snapshot_id=None):
# need to ensure the container name is not an empty
# string, but pass None to get the container name
# generated correctly
@@ -639,6 +648,7 @@ def volume_backup_create(request,
container=container_name if container_name else None,
name=name,
description=description,
+ snapshot_id=snapshot_id,
force=force)
return VolumeBackup(backup)
diff --git a/openstack_dashboard/dashboards/project/backups/forms.py b/openstack_dashboard/dashboards/project/backups/forms.py
index d6c6b15300..4f5dc8ea38 100644
--- a/openstack_dashboard/dashboards/project/backups/forms.py
+++ b/openstack_dashboard/dashboards/project/backups/forms.py
@@ -40,20 +40,58 @@ class CreateBackupForm(forms.SelfHandlingForm):
validators=[containers_utils.no_slash_validator],
required=False)
volume_id = forms.CharField(widget=forms.HiddenInput())
+ snapshot_id = forms.ThemableChoiceField(label=_("Backup Snapshot"),
+ required=False)
+
+ def __init__(self, request, *args, **kwargs):
+ super(CreateBackupForm, self).__init__(request, *args, **kwargs)
+ if kwargs['initial'].get('snapshot_id'):
+ snap_id = kwargs['initial']['snapshot_id']
+ try:
+ snapshot = api.cinder.volume_snapshot_get(request, snap_id)
+ self.fields['snapshot_id'].choices = [(snapshot.id,
+ snapshot.name)]
+ self.fields['snapshot_id'].initial = snap_id
+ except Exception:
+ redirect = reverse('horizon:project:snapshots:index')
+ exceptions.handle(request, _('Unable to fetch snapshot'),
+ redirect=redirect)
+ else:
+ try:
+ sop = {'volume_id': kwargs['initial']['volume_id']}
+ snapshots = api.cinder.volume_snapshot_list(request,
+ search_opts=sop)
+
+ snapshots.sort(key=operator.attrgetter('id', 'created_at'))
+ snapshotChoices = [[snapshot.id, snapshot.name]
+ for snapshot in snapshots]
+ if not snapshotChoices:
+ snapshotChoices.insert(0, ('',
+ _("No snapshot for this volume")))
+ else:
+ snapshotChoices.insert(
+ 0, ('',
+ _("Select snapshot to backup (Optional)")))
+ self.fields['snapshot_id'].choices = snapshotChoices
+
+ except Exception:
+ redirect = reverse('horizon:project:volumes:index')
+ exceptions.handle(request, _('Unable to fetch snapshots'),
+ redirect=redirect)
def handle(self, request, data):
-
try:
volume = api.cinder.volume_get(request, data['volume_id'])
+ snapshot_id = data['snapshot_id'] or None
force = False
if volume.status == 'in-use':
force = True
- backup = api.cinder.volume_backup_create(request,
- data['volume_id'],
- data['container_name'],
- data['name'],
- data['description'],
- force=force)
+ backup = api.cinder.volume_backup_create(
+ request, data['volume_id'],
+ data['container_name'], data['name'],
+ data['description'], force=force,
+ snapshot_id=snapshot_id
+ )
message = _('Creating volume backup "%s"') % data['name']
messages.info(request, message)
diff --git a/openstack_dashboard/dashboards/project/backups/tables.py b/openstack_dashboard/dashboards/project/backups/tables.py
index e4ce2ec69a..0995596a38 100644
--- a/openstack_dashboard/dashboards/project/backups/tables.py
+++ b/openstack_dashboard/dashboards/project/backups/tables.py
@@ -44,6 +44,24 @@ class BackupVolumeNameColumn(tables.Column):
return reverse(self.link, args=(volume_id,))
+class SnapshotColumn(tables.Column):
+ def get_raw_data(self, backup):
+ snapshot = backup.snapshot
+ if snapshot:
+ snapshot_name = snapshot.name
+ snapshot_name = html.escape(snapshot_name)
+ elif backup.snapshot_id:
+ snapshot_name = _("Unknown")
+ else:
+ return None
+ return safestring.mark_safe(snapshot_name)
+
+ def get_link_url(self, backup):
+ if backup.snapshot:
+ return reverse('horizon:project:snapshots:detail',
+ args=(backup.snapshot_id,))
+
+
class DeleteBackup(tables.DeleteAction):
help_text = _("Deleted volume backups are not recoverable.")
policy_rules = (("volume", "backup:delete"),)
@@ -103,6 +121,14 @@ class UpdateRow(tables.Row):
backup.volume_id)
except Exception:
pass
+ if backup.snapshot_id is not None:
+ try:
+ backup.snapshot = cinder.volume_snapshot_get(
+ request, backup.snapshot_id)
+ except Exception:
+ pass
+ else:
+ backup.snapshot = None
return backup
@@ -148,6 +174,9 @@ class BackupsTable(tables.DataTable):
volume_name = BackupVolumeNameColumn("name",
verbose_name=_("Volume Name"),
link="horizon:project:volumes:detail")
+ snapshot = SnapshotColumn("snapshot",
+ verbose_name=_("Snapshot"),
+ link="horizon:project:snapshots:detail")
class Meta(object):
name = "volume_backups"
diff --git a/openstack_dashboard/dashboards/project/backups/tabs.py b/openstack_dashboard/dashboards/project/backups/tabs.py
index 486fe0376a..54fd93c180 100644
--- a/openstack_dashboard/dashboards/project/backups/tabs.py
+++ b/openstack_dashboard/dashboards/project/backups/tabs.py
@@ -32,8 +32,19 @@ class BackupOverviewTab(tabs.Tab):
volume = cinder.volume_get(request, backup.volume_id)
except Exception:
volume = None
+ try:
+ if backup.snapshot_id:
+ snapshot = cinder.volume_snapshot_get(request,
+ backup.snapshot_id)
+ else:
+ snapshot = None
+ except Exception:
+ snapshot = None
+
return {'backup': backup,
- 'volume': volume}
+ 'volume': volume,
+ 'snapshot': snapshot}
+
except Exception:
redirect = reverse('horizon:project:backups:index')
exceptions.handle(self.request,
diff --git a/openstack_dashboard/dashboards/project/backups/templates/backups/_create_backup.html b/openstack_dashboard/dashboards/project/backups/templates/backups/_create_backup.html
index 9e265f1c6c..b480d7938d 100644
--- a/openstack_dashboard/dashboards/project/backups/templates/backups/_create_backup.html
+++ b/openstack_dashboard/dashboards/project/backups/templates/backups/_create_backup.html
@@ -9,6 +9,11 @@
You must have one of these services activated in order to create a backup.
{% endblocktrans %}
{% blocktrans %}
+ If a snapshot is specified here only the specified snapshot of the volume
+ will be backed up.
+
+ {% endblocktrans %}
+ {% blocktrans %}
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.
diff --git a/openstack_dashboard/dashboards/project/backups/templates/backups/_detail_overview.html b/openstack_dashboard/dashboards/project/backups/templates/backups/_detail_overview.html
index 11a3544f21..26e78b5185 100644
--- a/openstack_dashboard/dashboards/project/backups/templates/backups/_detail_overview.html
+++ b/openstack_dashboard/dashboards/project/backups/templates/backups/_detail_overview.html
@@ -20,6 +20,20 @@
{% endif %}
+ {% if backup.snapshot_id %}
+
{% trans "Snapshot" %}
+ {% if snapshot %}
+
+
+ {{ snapshot.name }}
+
+
+ {% elif backup.snapshot_id %}
+
+ {{ backup.snapshot_id }}
+ {% endif %}
+
+ {% endif %}
{% trans "Specs" %}
diff --git a/openstack_dashboard/dashboards/project/backups/templates/backups/create_backup.html b/openstack_dashboard/dashboards/project/backups/templates/backups/create_backup.html
index 8948af1f85..f9b220679e 100644
--- a/openstack_dashboard/dashboards/project/backups/templates/backups/create_backup.html
+++ b/openstack_dashboard/dashboards/project/backups/templates/backups/create_backup.html
@@ -3,5 +3,5 @@
{% block title %}{% trans "Create Volume Backup" %}{% endblock %}
{% block main %}
- {% include 'project/volumes/backups/_create_backup.html' %}
+ {% include 'project/backups/_create_backup.html' %}
{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/backups/tests.py b/openstack_dashboard/dashboards/project/backups/tests.py
index 4f13b20dbd..94d25f05c7 100644
--- a/openstack_dashboard/dashboards/project/backups/tests.py
+++ b/openstack_dashboard/dashboards/project/backups/tests.py
@@ -27,13 +27,15 @@ INDEX_URL = reverse('horizon:project:backups:index')
class VolumeBackupsViewTests(test.TestCase):
- @test.create_mocks({api.cinder: ('volume_list',
+ @test.create_mocks({api.cinder: ('volume_list', 'volume_snapshot_list',
'volume_backup_list_paged')})
def _test_backups_index_paginated(self, marker, sort_dir, backups, url,
has_more, has_prev):
self.mock_volume_backup_list_paged.return_value = [backups,
has_more, has_prev]
self.mock_volume_list.return_value = self.cinder_volumes.list()
+ self.mock_volume_snapshot_list.return_value \
+ = self.cinder_volume_snapshots.list()
res = self.client.get(urlunquote(url))
@@ -43,12 +45,14 @@ class VolumeBackupsViewTests(test.TestCase):
test.IsHttpRequest(), marker=marker, sort_dir=sort_dir,
paginate=True)
self.mock_volume_list.assert_called_once_with(test.IsHttpRequest())
-
+ self.mock_volume_snapshot_list.assert_called_once_with(
+ test.IsHttpRequest())
return res
@override_settings(API_RESULT_PAGE_SIZE=1)
def test_backups_index_paginated(self):
backups = self.cinder_volume_backups.list()
+ expected_snapshosts = self.cinder_volume_snapshots.list()
size = settings.API_RESULT_PAGE_SIZE
base_url = INDEX_URL
next = backup_tables.BackupsTable._meta.pagination_param
@@ -71,7 +75,7 @@ class VolumeBackupsViewTests(test.TestCase):
has_more=True, has_prev=True)
result = res.context['volume_backups_table'].data
self.assertItemsEqual(result, expected_backups)
-
+ self.assertEqual(result[0].snapshot.id, expected_snapshosts[1].id)
# get last page
expected_backups = backups[-size:]
marker = expected_backups[0].id
@@ -110,6 +114,7 @@ class VolumeBackupsViewTests(test.TestCase):
self.assertItemsEqual(result, expected_backups)
@test.create_mocks({api.cinder: ('volume_backup_create',
+ 'volume_snapshot_list',
'volume_get')})
def test_create_backup_available(self):
volume = self.cinder_volumes.first()
@@ -131,7 +136,9 @@ class VolumeBackupsViewTests(test.TestCase):
self.assertNoFormErrors(res)
self.assertMessageCount(error=0, warning=0)
self.assertRedirectsNoFollow(res, INDEX_URL)
-
+ self.mock_volume_snapshot_list.assert_called_once_with(
+ test.IsHttpRequest(),
+ search_opts={'volume_id': volume.id})
self.mock_volume_get.assert_called_once_with(test.IsHttpRequest(),
volume.id)
self.mock_volume_backup_create.assert_called_once_with(
@@ -140,18 +147,97 @@ class VolumeBackupsViewTests(test.TestCase):
backup.container_name,
backup.name,
backup.description,
- force=False)
+ force=False,
+ snapshot_id=None)
- @test.create_mocks({api.cinder: ('volume_backup_create',
- 'volume_get')})
+ @test.create_mocks(
+ {api.cinder: ('volume_backup_create', 'volume_snapshot_get',
+ 'volume_get')})
+ def test_create_backup_from_snapshot_table(self):
+ backup = self.cinder_volume_backups.list()[1]
+ volume = self.cinder_volumes.list()[4]
+ snapshot = self.cinder_volume_snapshots.list()[1]
+ self.mock_volume_backup_create.return_value = backup
+ self.mock_volume_get.return_value = volume
+ self.mock_volume_snapshot_get.return_value = snapshot
+ formData = {'method': 'CreateBackupForm',
+ 'tenant_id': self.tenant.id,
+ 'volume_id': volume.id,
+ 'container_name': backup.container_name,
+ 'name': backup.name,
+ 'snapshot_id': backup.snapshot_id,
+ 'description': backup.description}
+ url = reverse('horizon:project:volumes:create_snapshot_backup',
+ args=[backup.volume_id, backup.snapshot_id])
+ res = self.client.post(url, formData)
+
+ self.assertNoFormErrors(res)
+ self.assertMessageCount(error=0, warning=0)
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ self.mock_volume_get.assert_called_once_with(test.IsHttpRequest(),
+ backup.volume_id)
+ self.mock_volume_backup_create.assert_called_once_with(
+ test.IsHttpRequest(),
+ backup.volume_id,
+ backup.container_name,
+ backup.name,
+ backup.description,
+ force=False,
+ snapshot_id=backup.snapshot_id)
+
+ @test.create_mocks(
+ {api.cinder: ('volume_backup_create',
+ 'volume_snapshot_list',
+ 'volume_get')})
+ def test_create_backup_from_snapshot_volume_table(self):
+ volume = self.cinder_volumes.list()[4]
+ backup = self.cinder_volume_backups.list()[1]
+ snapshots = self.cinder_volume_snapshots.list()[1:3]
+ self.mock_volume_backup_create.return_value = backup
+ self.mock_volume_get.return_value = volume
+ self.mock_volume_snapshot_list.return_value = snapshots
+ formData = {'method': 'CreateBackupForm',
+ 'tenant_id': self.tenant.id,
+ 'volume_id': volume.id,
+ 'container_name': backup.container_name,
+ 'name': backup.name,
+ 'snapshot_id': snapshots[0].id,
+ 'description': backup.description}
+ url = reverse('horizon:project:volumes:create_backup',
+ args=[backup.volume_id])
+
+ res = self.client.post(url, formData)
+ self.assertNoFormErrors(res)
+ self.mock_volume_snapshot_list.assert_called_once_with(
+ test.IsHttpRequest(),
+ search_opts={'volume_id': volume.id})
+ self.assertMessageCount(error=0, warning=0)
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ self.mock_volume_get.assert_called_once_with(test.IsHttpRequest(),
+ backup.volume_id)
+ self.mock_volume_backup_create.assert_called_once_with(
+ test.IsHttpRequest(),
+ backup.volume_id,
+ backup.container_name,
+ backup.name,
+ backup.description,
+ force=False,
+ snapshot_id=backup.snapshot_id)
+
+ @test.create_mocks(
+ {api.cinder: ('volume_backup_create', 'volume_snapshot_list',
+ 'volume_get')})
def test_create_backup_in_use(self):
# The third volume in the cinder test volume data is in-use
volume = self.cinder_volumes.list()[2]
- backup = self.cinder_volume_backups.first()
+ backup = self.cinder_volume_backups.first()
+ snapshots = []
self.mock_volume_get.return_value = volume
self.mock_volume_backup_create.return_value = backup
-
+ self.mock_volume_snapshot_list.return_value = snapshots
formData = {'method': 'CreateBackupForm',
'tenant_id': self.tenant.id,
'volume_id': volume.id,
@@ -160,8 +246,11 @@ class VolumeBackupsViewTests(test.TestCase):
'description': backup.description}
url = reverse('horizon:project:volumes:create_backup',
args=[volume.id])
- res = self.client.post(url, formData)
+ res = self.client.post(url, formData)
+ self.mock_volume_snapshot_list.assert_called_once_with(
+ test.IsHttpRequest(),
+ search_opts={'volume_id': volume.id})
self.assertNoFormErrors(res)
self.assertMessageCount(error=0, warning=0)
self.assertRedirectsNoFollow(res, INDEX_URL)
@@ -173,21 +262,24 @@ class VolumeBackupsViewTests(test.TestCase):
backup.container_name,
backup.name,
backup.description,
- force=True)
+ force=True,
+ snapshot_id=None)
@test.create_mocks({api.cinder: ('volume_list',
+ 'volume_snapshot_list',
'volume_backup_list_paged',
'volume_backup_delete')})
def test_delete_volume_backup(self):
vol_backups = self.cinder_volume_backups.list()
volumes = self.cinder_volumes.list()
backup = self.cinder_volume_backups.first()
+ snapshots = self.cinder_volume_snapshots.list()
self.mock_volume_backup_list_paged.return_value = [vol_backups,
False, False]
self.mock_volume_list.return_value = volumes
self.mock_volume_backup_delete.return_value = None
-
+ self.mock_volume_snapshot_list.return_value = snapshots
formData = {'action':
'volume_backups__delete__%s' % backup.id}
res = self.client.post(INDEX_URL, formData)
@@ -198,6 +290,8 @@ class VolumeBackupsViewTests(test.TestCase):
test.IsHttpRequest(), marker=None, sort_dir='desc',
paginate=True)
self.mock_volume_list.assert_called_once_with(test.IsHttpRequest())
+ self.mock_volume_snapshot_list.assert_called_once_with(
+ test.IsHttpRequest())
self.mock_volume_backup_delete.assert_called_once_with(
test.IsHttpRequest(), backup.id)
@@ -221,6 +315,31 @@ class VolumeBackupsViewTests(test.TestCase):
self.mock_volume_get.assert_called_once_with(
test.IsHttpRequest(), backup.volume_id)
+ @test.create_mocks({api.cinder: ('volume_backup_get',
+ 'volume_snapshot_get',
+ 'volume_get')})
+ def test_volume_backup_detail_get_with_snapshot(self):
+ backup = self.cinder_volume_backups.list()[1]
+ volume = self.cinder_volumes.get(id=backup.volume_id)
+
+ self.mock_volume_backup_get.return_value = backup
+ self.mock_volume_get.return_value = volume
+ self.mock_volume_snapshot_get.return_value \
+ = self.cinder_volume_snapshots.list()[1]
+ url = reverse('horizon:project:backups:detail',
+ args=[backup.id])
+ res = self.client.get(url)
+
+ self.assertTemplateUsed(res, 'horizon/common/_detail.html')
+ self.assertEqual(res.context['backup'].id, backup.id)
+ self.assertEqual(res.context['snapshot'].id, backup.snapshot_id)
+ self.mock_volume_backup_get.assert_called_once_with(
+ test.IsHttpRequest(), backup.id)
+ self.mock_volume_get.assert_called_once_with(
+ test.IsHttpRequest(), backup.volume_id)
+ self.mock_volume_snapshot_get.assert_called_once_with(
+ test.IsHttpRequest(), backup.snapshot_id)
+
@test.create_mocks({api.cinder: ('volume_backup_get',)})
def test_volume_backup_detail_get_with_exception(self):
# Test to verify redirect if get volume backup fails
diff --git a/openstack_dashboard/dashboards/project/backups/views.py b/openstack_dashboard/dashboards/project/backups/views.py
index 997594f380..481308b875 100644
--- a/openstack_dashboard/dashboards/project/backups/views.py
+++ b/openstack_dashboard/dashboards/project/backups/views.py
@@ -48,8 +48,11 @@ class BackupsView(tables.DataTableView, tables.PagedTableMixin,
paginate=True)
volumes = api.cinder.volume_list(self.request)
volumes = dict((v.id, v) for v in volumes)
+ snapshots = api.cinder.volume_snapshot_list(self.request)
+ snapshots = dict((s.id, s) for s in snapshots)
for backup in backups:
backup.volume = volumes.get(backup.volume_id)
+ backup.snapshot = snapshots.get(backup.snapshot_id)
except Exception:
backups = []
exceptions.handle(self.request, _("Unable to retrieve "
@@ -73,6 +76,9 @@ class CreateBackupView(forms.ModalFormView):
return context
def get_initial(self):
+ if self.kwargs.get('snapshot_id'):
+ return {"volume_id": self.kwargs["volume_id"],
+ "snapshot_id": self.kwargs["snapshot_id"]}
return {"volume_id": self.kwargs["volume_id"]}
diff --git a/openstack_dashboard/dashboards/project/snapshots/tables.py b/openstack_dashboard/dashboards/project/snapshots/tables.py
index 2c66dd2c41..7c73b3d2d5 100644
--- a/openstack_dashboard/dashboards/project/snapshots/tables.py
+++ b/openstack_dashboard/dashboards/project/snapshots/tables.py
@@ -101,6 +101,8 @@ class DeleteVolumeSnapshot(policy.PolicyTargetMixin, tables.DeleteAction):
# Can't delete snapshot if part of group snapshot
if getattr(datum, 'group_snapshot_id', None):
return False
+ if datum.status == 'backing-up':
+ return False
return True
@@ -142,6 +144,23 @@ class CreateVolumeFromSnapshot(tables.LinkAction):
return False
+class CreateBackup(policy.PolicyTargetMixin, tables.LinkAction):
+ name = "backups"
+ verbose_name = _("Create Backup")
+ url = "horizon:project:volumes:create_snapshot_backup"
+ classes = ("ajax-modal",)
+ policy_rules = (("volume", "backup:create"),)
+
+ def get_link_url(self, datum):
+ snap_id = self.table.get_object_id(datum)
+ url = reverse(self.url, args=(datum.volume_id, snap_id))
+ return url
+
+ def allowed(self, request, snapshot=None):
+ return (cinder.volume_backup_supported(request) and
+ snapshot.status == 'available')
+
+
class UpdateMetadata(tables.LinkAction):
name = "update_metadata"
verbose_name = _("Update Metadata")
@@ -230,7 +249,7 @@ class VolumeDetailsSnapshotsTable(volume_tables.VolumesTableBase):
launch_actions = (LaunchSnapshotNG,) + launch_actions
row_actions = ((CreateVolumeFromSnapshot,) + launch_actions +
- (EditVolumeSnapshot, DeleteVolumeSnapshot,
+ (EditVolumeSnapshot, DeleteVolumeSnapshot, CreateBackup,
UpdateMetadata))
row_class = UpdateRow
status_columns = ("status",)
diff --git a/openstack_dashboard/dashboards/project/volumes/urls.py b/openstack_dashboard/dashboards/project/volumes/urls.py
index 191ae283e0..98bbd61e98 100644
--- a/openstack_dashboard/dashboards/project/volumes/urls.py
+++ b/openstack_dashboard/dashboards/project/volumes/urls.py
@@ -42,6 +42,9 @@ urlpatterns = [
url(r'^(?P[^/]+)/create_backup/$',
backup_views.CreateBackupView.as_view(),
name='create_backup'),
+ url(r'^(?P[^/]+)/create_backup/(?P[^/]+)$',
+ backup_views.CreateBackupView.as_view(),
+ name='create_snapshot_backup'),
url(r'^(?P[^/]+)/$',
views.DetailView.as_view(),
name='detail'),
diff --git a/openstack_dashboard/test/test_data/cinder_data.py b/openstack_dashboard/test/test_data/cinder_data.py
index cbc5d03fcc..b54a051624 100644
--- a/openstack_dashboard/test/test_data/cinder_data.py
+++ b/openstack_dashboard/test/test_data/cinder_data.py
@@ -204,6 +204,7 @@ def data(TEST):
'display_name': 'test snapshot',
'display_description': 'volume snapshot',
'size': 40,
+ 'created_at': '2014-01-27 10:30:00',
'status': 'available',
'volume_id': '11023e92-8008-4c8b-8059-7f2293ff3887'})
snapshot2 = vol_snaps.Snapshot(
@@ -212,6 +213,7 @@ def data(TEST):
'name': '',
'description': 'v2 volume snapshot description',
'size': 80,
+ 'created_at': '2014-01-27 10:30:00',
'status': 'available',
'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'})
snapshot3 = vol_snaps.Snapshot(
@@ -220,6 +222,7 @@ def data(TEST):
'name': '',
'description': 'v2 volume snapshot description 2',
'size': 80,
+ 'created_at': '2014-01-27 10:30:00',
'status': 'available',
'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'})
snapshot4 = vol_snaps.Snapshot(
@@ -228,6 +231,7 @@ def data(TEST):
'name': '',
'description': 'v2 volume snapshot with metadata description',
'size': 80,
+ 'created_at': '2014-01-27 10:30:00',
'status': 'available',
'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234',
'metadata': {'snapshot_meta_key': 'snapshot_meta_value'}})
@@ -269,6 +273,7 @@ def data(TEST):
'size': 10,
'status': 'available',
'container_name': 'volumebackups',
+ 'snapshot_id': None,
'volume_id': '11023e92-8008-4c8b-8059-7f2293ff3887'})
volume_backup2 = vol_backups.VolumeBackup(
@@ -278,6 +283,7 @@ def data(TEST):
'description': 'volume backup 2',
'size': 20,
'status': 'available',
+ 'snapshot_id': snapshot2.id,
'container_name': 'volumebackups',
'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'})
@@ -287,6 +293,7 @@ def data(TEST):
'name': 'backup3',
'description': 'volume backup 3',
'size': 20,
+ 'snapshot_id': None,
'status': 'available',
'container_name': 'volumebackups',
'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'})
diff --git a/releasenotes/notes/bp-volume-snapshot-backups-54e4d18633fd4c5d.yaml b/releasenotes/notes/bp-volume-snapshot-backups-54e4d18633fd4c5d.yaml
new file mode 100644
index 0000000000..11548c0884
--- /dev/null
+++ b/releasenotes/notes/bp-volume-snapshot-backups-54e4d18633fd4c5d.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - Enabled horizon to make use of cinder's feature to
+ backup up snapshots of block storage volumes.
+