summaryrefslogtreecommitdiff
path: root/cinderclient
diff options
context:
space:
mode:
authorAlan Bishop <abishop@redhat.com>2021-01-11 13:05:11 -0800
committerAlan Bishop <abishop@redhat.com>2021-01-11 13:05:11 -0800
commit7e3566ed04c8f664f6e1df0614499989f6b3560a (patch)
treee6c22ef2c34214074b4c5193ca89c539995fc5cd /cinderclient
parent1abc1b5d404c523a696f7186bc4c4b6fc7407cad (diff)
downloadpython-cinderclient-7e3566ed04c8f664f6e1df0614499989f6b3560a.tar.gz
Support backup-restore to a specific volume type or AZ
Enhance the 'backup-restore' shell command to support restoring a backup to a newly created volume of a specific volume type and/or in a different AZ. New '--volume-type' and '--availability-zone' arguments leverage the existing cinder API's ability to create a volume from a backup, which was added in microversion v3.47. The shell code is a new v3 implementation, and it drops support for the v2 command's deprecated '--volume-id' argument. Change-Id: Ic6645d3b973f8487903c5f57e936ba3b4b3bf005
Diffstat (limited to 'cinderclient')
-rw-r--r--cinderclient/tests/unit/v3/test_shell.py157
-rw-r--r--cinderclient/v3/shell.py68
2 files changed, 225 insertions, 0 deletions
diff --git a/cinderclient/tests/unit/v3/test_shell.py b/cinderclient/tests/unit/v3/test_shell.py
index 0332ae3..314259d 100644
--- a/cinderclient/tests/unit/v3/test_shell.py
+++ b/cinderclient/tests/unit/v3/test_shell.py
@@ -1641,3 +1641,160 @@ class ShellTest(utils.TestCase):
'629632e7-99d2-4c40-9ae3-106fa3b1c9b7')
self.assert_called(
'DELETE', 'v3/default-types/629632e7-99d2-4c40-9ae3-106fa3b1c9b7')
+
+ def test_restore(self):
+ self.run_command('backup-restore 1234')
+ self.assert_called('POST', '/backups/1234/restore')
+
+ def test_restore_with_name(self):
+ self.run_command('backup-restore 1234 --name restore_vol')
+ expected = {'restore': {'volume_id': None, 'name': 'restore_vol'}}
+ self.assert_called('POST', '/backups/1234/restore',
+ body=expected)
+
+ def test_restore_with_name_error(self):
+ self.assertRaises(exceptions.CommandError, self.run_command,
+ 'backup-restore 1234 --volume fake_vol --name '
+ 'restore_vol')
+
+ def test_restore_with_az(self):
+ self.run_command('--os-volume-api-version 3.47 backup-restore 1234 '
+ '--name restore_vol --availability-zone restore_az')
+ expected = {'volume': {'size': 10,
+ 'name': 'restore_vol',
+ 'availability_zone': 'restore_az',
+ 'backup_id': '1234',
+ 'metadata': {},
+ 'imageRef': None,
+ 'source_volid': None,
+ 'consistencygroup_id': None,
+ 'snapshot_id': None,
+ 'volume_type': None,
+ 'description': None}}
+ self.assert_called('POST', '/volumes', body=expected)
+
+ def test_restore_with_az_microversion_error(self):
+ self.assertRaises(exceptions.UnsupportedAttribute, self.run_command,
+ '--os-volume-api-version 3.46 backup-restore 1234 '
+ '--name restore_vol --availability-zone restore_az')
+
+ def test_restore_with_volume_type(self):
+ self.run_command('--os-volume-api-version 3.47 backup-restore 1234 '
+ '--name restore_vol --volume-type restore_type')
+ expected = {'volume': {'size': 10,
+ 'name': 'restore_vol',
+ 'volume_type': 'restore_type',
+ 'backup_id': '1234',
+ 'metadata': {},
+ 'imageRef': None,
+ 'source_volid': None,
+ 'consistencygroup_id': None,
+ 'snapshot_id': None,
+ 'availability_zone': None,
+ 'description': None}}
+ self.assert_called('POST', '/volumes', body=expected)
+
+ def test_restore_with_volume_type_microversion_error(self):
+ self.assertRaises(exceptions.UnsupportedAttribute, self.run_command,
+ '--os-volume-api-version 3.46 backup-restore 1234 '
+ '--name restore_vol --volume-type restore_type')
+
+ def test_restore_with_volume_type_and_az_no_name(self):
+ self.run_command('--os-volume-api-version 3.47 backup-restore 1234 '
+ '--volume-type restore_type '
+ '--availability-zone restore_az')
+ expected = {'volume': {'size': 10,
+ 'name': 'restore_backup_1234',
+ 'volume_type': 'restore_type',
+ 'availability_zone': 'restore_az',
+ 'backup_id': '1234',
+ 'metadata': {},
+ 'imageRef': None,
+ 'source_volid': None,
+ 'consistencygroup_id': None,
+ 'snapshot_id': None,
+ 'description': None}}
+ self.assert_called('POST', '/volumes', body=expected)
+
+ @ddt.data(
+ {
+ 'volume': '1234',
+ 'name': None,
+ 'volume_type': None,
+ 'availability_zone': None,
+ }, {
+ 'volume': '1234',
+ 'name': 'ignored',
+ 'volume_type': None,
+ 'availability_zone': None,
+ }, {
+ 'volume': None,
+ 'name': 'sample-volume',
+ 'volume_type': 'sample-type',
+ 'availability_zone': None,
+ }, {
+ 'volume': None,
+ 'name': 'sample-volume',
+ 'volume_type': None,
+ 'availability_zone': 'az1',
+ }, {
+ 'volume': None,
+ 'name': 'sample-volume',
+ 'volume_type': None,
+ 'availability_zone': 'different-az',
+ }, {
+ 'volume': None,
+ 'name': None,
+ 'volume_type': None,
+ 'availability_zone': 'different-az',
+ },
+ )
+ @ddt.unpack
+ @mock.patch('cinderclient.utils.print_dict')
+ @mock.patch('cinderclient.tests.unit.v2.fakes._stub_restore')
+ def test_do_backup_restore(self,
+ mock_stub_restore,
+ mock_print_dict,
+ volume,
+ name,
+ volume_type,
+ availability_zone):
+
+ # Restore from the fake '1234' backup.
+ cmd = '--os-volume-api-version 3.47 backup-restore 1234'
+
+ if volume:
+ cmd += ' --volume %s' % volume
+ if name:
+ cmd += ' --name %s' % name
+ if volume_type:
+ cmd += ' --volume-type %s' % volume_type
+ if availability_zone:
+ cmd += ' --availability-zone %s' % availability_zone
+
+ if name or volume:
+ volume_name = 'sample-volume'
+ else:
+ volume_name = 'restore_backup_1234'
+
+ mock_stub_restore.return_value = {'volume_id': '1234',
+ 'volume_name': volume_name}
+
+ self.run_command(cmd)
+
+ # Check whether mock_stub_restore was called in order to determine
+ # whether the restore command invoked the backup-restore API. If
+ # mock_stub_restore was not called then this indicates the command
+ # invoked the volume-create API to restore the backup to a new volume
+ # of a specific volume type, or in a different AZ (the fake '1234'
+ # backup is in az1).
+ if volume_type or availability_zone == 'different-az':
+ mock_stub_restore.assert_not_called()
+ else:
+ mock_stub_restore.assert_called_once()
+
+ mock_print_dict.assert_called_once_with({
+ 'backup_id': '1234',
+ 'volume_id': '1234',
+ 'volume_name': volume_name,
+ })
diff --git a/cinderclient/v3/shell.py b/cinderclient/v3/shell.py
index eaded7e..3065352 100644
--- a/cinderclient/v3/shell.py
+++ b/cinderclient/v3/shell.py
@@ -218,6 +218,74 @@ def do_backup_list(cs, args):
AppendFilters.filters = []
+@utils.arg('backup', metavar='<backup>',
+ help='Name or ID of backup to restore.')
+@utils.arg('--volume', metavar='<volume>',
+ default=None,
+ help='Name or ID of existing volume to which to restore. '
+ 'This is mutually exclusive with --name and takes priority. '
+ 'Default=None.')
+@utils.arg('--name', metavar='<name>',
+ default=None,
+ help='Use the name for new volume creation to restore. '
+ 'This is mutually exclusive with --volume and --volume '
+ 'takes priority. '
+ 'Default=None.')
+@utils.arg('--volume-type',
+ metavar='<volume-type>',
+ default=None,
+ start_version='3.47',
+ help='Volume type for the new volume creation to restore. This '
+ 'option is not valid when used with the "volume" option. '
+ 'Default=None.')
+@utils.arg('--availability-zone', metavar='<AZ>',
+ default=None,
+ start_version='3.47',
+ help='AZ for the new volume creation to restore. By default it '
+ 'will be the same as backup AZ. This option is not valid when '
+ 'used with the "volume" option. Default=None.')
+def do_backup_restore(cs, args):
+ """Restores a backup."""
+ if args.volume:
+ volume_id = utils.find_volume(cs, args.volume).id
+ if args.name:
+ args.name = None
+ print('Mutually exclusive options are specified simultaneously: '
+ '"volume" and "name". The volume option takes priority.')
+ else:
+ volume_id = None
+
+ volume_type = getattr(args, 'volume_type', None)
+ az = getattr(args, 'availability_zone', None)
+ if (volume_type or az) and args.volume:
+ msg = ('The "volume-type" and "availability-zone" options are not '
+ 'valid when used with the "volume" option.')
+ raise exceptions.ClientException(code=1, message=msg)
+
+ backup = shell_utils.find_backup(cs, args.backup)
+ info = {"backup_id": backup.id}
+
+ if volume_type or (az and az != backup.availability_zone):
+ # Implement restoring a backup to a newly created volume of a
+ # specific volume type or in a different AZ by using the
+ # volume-create API. The default volume name matches the pattern
+ # cinder uses (see I23730834058d88e30be62624ada3b24cdaeaa6f3).
+ volume_name = args.name or 'restore_backup_%s' % backup.id
+ volume = cs.volumes.create(size=backup.size,
+ name=volume_name,
+ volume_type=volume_type,
+ availability_zone=az,
+ backup_id=backup.id)
+ info['volume_id'] = volume._info['id']
+ info['volume_name'] = volume_name
+ else:
+ restore = cs.restores.restore(backup.id, volume_id, args.name)
+ info.update(restore._info)
+ info.pop('links', None)
+
+ utils.print_dict(info)
+
+
@utils.arg('--detail',
action='store_true',
help='Show detailed information about pools.')