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. +