diff --git a/openstack_dashboard/api/cinder.py b/openstack_dashboard/api/cinder.py index 03ac5aee34f..d3b659926a3 100644 --- a/openstack_dashboard/api/cinder.py +++ b/openstack_dashboard/api/cinder.py @@ -585,8 +585,9 @@ def volume_backup_get(request, backup_id): return VolumeBackup(backup) -def volume_backup_list(request): - backups, _, __ = volume_backup_list_paged(request, paginate=False) +def volume_backup_list(request, search_opts=None): + backups, _, __ = volume_backup_list_paged(request, paginate=False, + search_opts=search_opts) return backups @@ -625,7 +626,7 @@ def volume_backup_list_paged_with_page_menu(request, page_number=1, @profiler.trace def volume_backup_list_paged(request, marker=None, paginate=False, - sort_dir="desc"): + sort_dir="desc", search_opts=None): has_more_data = False has_prev_data = False backups = [] @@ -642,13 +643,13 @@ def volume_backup_list_paged(request, marker=None, paginate=False, sort = 'created_at:' + sort_dir for b in c_client.backups.list(limit=page_size + 1, marker=marker, - sort=sort): + sort=sort, search_opts=search_opts): backups.append(VolumeBackup(b)) backups, has_more_data, has_prev_data = update_pagination( backups, page_size, marker, sort_dir) else: - for b in c_client.backups.list(): + for b in c_client.backups.list(search_opts=search_opts): backups.append(VolumeBackup(b)) return backups, has_more_data, has_prev_data @@ -661,6 +662,7 @@ def volume_backup_create(request, name, description, force=False, + incremental=False, snapshot_id=None): # need to ensure the container name is not an empty # string, but pass None to get the container name @@ -671,6 +673,7 @@ def volume_backup_create(request, name=name, description=description, snapshot_id=snapshot_id, + incremental=incremental, force=force) return VolumeBackup(backup) diff --git a/openstack_dashboard/dashboards/project/backups/forms.py b/openstack_dashboard/dashboards/project/backups/forms.py index 9a97e9a0c14..c6f45d18869 100644 --- a/openstack_dashboard/dashboards/project/backups/forms.py +++ b/openstack_dashboard/dashboards/project/backups/forms.py @@ -42,9 +42,30 @@ class CreateBackupForm(forms.SelfHandlingForm): volume_id = forms.CharField(widget=forms.HiddenInput()) snapshot_id = forms.ThemableChoiceField(label=_("Backup Snapshot"), required=False) + incremental = forms.BooleanField( + label=_("Incremental"), + required=False, + help_text=_("By default, a backup is created as a full backup. " + "Check this to do an incremental backup from latest " + "backup. Only available if a prior backup exists.")) def __init__(self, request, *args, **kwargs): super().__init__(request, *args, **kwargs) + search_opts = {"volume_id": kwargs['initial']['volume_id'], + "status": "available"} + try: + if not api.cinder.volume_backup_list(request, + search_opts=search_opts): + self.fields.pop('incremental') + except Exception: + # Do not include incremental if list of prior backups fails + self.fields.pop('incremental') + msg = _('Unable to retrieve volume backup list ' + 'for volume "%s", so incremental ' + 'backup is disabled.') % search_opts['volume_id'] + + exceptions.handle(self.request, msg) + if kwargs['initial'].get('snapshot_id'): snap_id = kwargs['initial']['snapshot_id'] try: @@ -84,12 +105,14 @@ def handle(self, request, data): volume = api.cinder.volume_get(request, data['volume_id']) snapshot_id = data['snapshot_id'] or None force = False + incremental = data.get('incremental', 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, + incremental=incremental, snapshot_id=snapshot_id ) diff --git a/openstack_dashboard/dashboards/project/backups/tests.py b/openstack_dashboard/dashboards/project/backups/tests.py index 4a93d782ad2..035158a18b4 100644 --- a/openstack_dashboard/dashboards/project/backups/tests.py +++ b/openstack_dashboard/dashboards/project/backups/tests.py @@ -132,11 +132,13 @@ def test_backups_index_paginated_prev_page(self): self.assertCountEqual(result, expected_backups) @test.create_mocks({api.cinder: ('volume_backup_create', + 'volume_backup_list', 'volume_snapshot_list', 'volume_get')}) def test_create_backup_available(self): volume = self.cinder_volumes.first() backup = self.cinder_volume_backups.first() + self.mock_volume_backup_list.return_value = [] self.mock_volume_get.return_value = volume self.mock_volume_backup_create.return_value = backup @@ -166,17 +168,61 @@ def test_create_backup_available(self): backup.name, backup.description, force=False, + incremental=False, + snapshot_id=None) + + @test.create_mocks({api.cinder: ('volume_backup_create', + 'volume_backup_list', + 'volume_snapshot_list', + 'volume_get')}) + def test_create_backup_available_incremental(self): + volume = self.cinder_volumes.first() + backup = self.cinder_volume_backups.list()[1] + prior_backups = [self.cinder_volume_backups.list()[0]] + self.mock_volume_backup_list.return_value = prior_backups + + self.mock_volume_get.return_value = volume + self.mock_volume_backup_create.return_value = backup + + formData = {'method': 'CreateBackupForm', + 'tenant_id': self.tenant.id, + 'volume_id': volume.id, + 'container_name': backup.container_name, + 'name': backup.name, + 'incremental': True, + 'description': backup.description} + url = reverse('horizon:project:volumes:create_backup', + args=[volume.id]) + res = self.client.post(url, formData) + + 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( + test.IsHttpRequest(), + volume.id, + backup.container_name, + backup.name, + backup.description, + force=False, + incremental=True, snapshot_id=None) @test.create_mocks( {api.cinder: ('volume_backup_create', 'volume_snapshot_get', - 'volume_get')}) + 'volume_get', 'volume_backup_list')}) 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_backup_list.return_value = [] self.mock_volume_snapshot_get.return_value = snapshot formData = {'method': 'CreateBackupForm', 'tenant_id': self.tenant.id, @@ -202,18 +248,20 @@ def test_create_backup_from_snapshot_table(self): backup.name, backup.description, force=False, + incremental=False, snapshot_id=backup.snapshot_id) @test.create_mocks( {api.cinder: ('volume_backup_create', 'volume_snapshot_list', - 'volume_get')}) + 'volume_get', 'volume_backup_list')}) 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_backup_list.return_value = [] self.mock_volume_snapshot_list.return_value = snapshots formData = {'method': 'CreateBackupForm', 'tenant_id': self.tenant.id, @@ -242,11 +290,12 @@ def test_create_backup_from_snapshot_volume_table(self): backup.name, backup.description, force=False, + incremental=False, snapshot_id=backup.snapshot_id) @test.create_mocks( {api.cinder: ('volume_backup_create', 'volume_snapshot_list', - 'volume_get')}) + 'volume_get', 'volume_backup_list')}) 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] @@ -256,6 +305,7 @@ def test_create_backup_in_use(self): self.mock_volume_get.return_value = volume self.mock_volume_backup_create.return_value = backup self.mock_volume_snapshot_list.return_value = snapshots + self.mock_volume_backup_list.return_value = [] formData = {'method': 'CreateBackupForm', 'tenant_id': self.tenant.id, 'volume_id': volume.id, @@ -281,7 +331,92 @@ def test_create_backup_in_use(self): backup.name, backup.description, force=True, - snapshot_id=None) + snapshot_id=None, + incremental=False) + + @test.create_mocks( + {api.cinder: ('volume_backup_create', 'volume_snapshot_list', + 'volume_get', 'volume_backup_list')}) + def test_create_backup_in_use_incremental(self): + volume = self.cinder_volumes.list()[2] + + backup = self.cinder_volume_backups.list()[1] + snapshots = [] + self.mock_volume_get.return_value = volume + self.mock_volume_backup_create.return_value = backup + self.mock_volume_snapshot_list.return_value = snapshots + prior_backups = [self.cinder_volume_backups.list()[0]] + self.mock_volume_backup_list.return_value = prior_backups + formData = {'method': 'CreateBackupForm', + 'tenant_id': self.tenant.id, + 'volume_id': volume.id, + 'container_name': backup.container_name, + 'name': backup.name, + 'incremental': True, + 'description': backup.description} + url = reverse('horizon:project:volumes:create_backup', + args=[volume.id]) + + 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) + self.mock_volume_get.assert_called_once_with(test.IsHttpRequest(), + volume.id) + self.mock_volume_backup_create.assert_called_once_with( + test.IsHttpRequest(), + volume.id, + backup.container_name, + backup.name, + backup.description, + force=True, + snapshot_id=None, + incremental=True) + + @test.create_mocks( + {api.cinder: ('volume_backup_create', 'volume_snapshot_list', + 'volume_get', 'volume_backup_list')}) + def test_create_backup_in_use_incremental_set_false(self): + volume = self.cinder_volumes.list()[2] + + backup = self.cinder_volume_backups.list()[1] + snapshots = [] + self.mock_volume_get.return_value = volume + self.mock_volume_backup_create.return_value = backup + self.mock_volume_snapshot_list.return_value = snapshots + prior_backups = [self.cinder_volume_backups.list()[0]] + self.mock_volume_backup_list.return_value = prior_backups + formData = {'method': 'CreateBackupForm', + 'tenant_id': self.tenant.id, + 'volume_id': volume.id, + 'container_name': backup.container_name, + 'name': backup.name, + 'incremental': False, + 'description': backup.description} + url = reverse('horizon:project:volumes:create_backup', + args=[volume.id]) + + 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) + self.mock_volume_get.assert_called_once_with(test.IsHttpRequest(), + volume.id) + self.mock_volume_backup_create.assert_called_once_with( + test.IsHttpRequest(), + volume.id, + backup.container_name, + backup.name, + backup.description, + force=True, + snapshot_id=None, + incremental=False) @test.create_mocks({api.cinder: ('volume_list', 'volume_snapshot_list',