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
This commit is contained in:
Alan Bishop 2021-01-11 13:05:11 -08:00
parent 1abc1b5d40
commit 7e3566ed04
3 changed files with 232 additions and 0 deletions

View File

@ -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,
})

View File

@ -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.')

View File

@ -0,0 +1,7 @@
---
features:
- |
Enhance the ``backup-restore`` shell command to support restoring to a new
volume created with a specific volume type and/or in a different AZ. New
``--volume-type`` and ``--availability-zone`` arguments are compatible with
cinder API microversion v3.47 onward.