diff options
-rw-r--r-- | cinderclient/api_versions.py | 2 | ||||
-rw-r--r-- | cinderclient/tests/unit/v3/test_shell.py | 157 | ||||
-rw-r--r-- | cinderclient/v3/shell.py | 68 | ||||
-rw-r--r-- | releasenotes/notes/enhance-backup-restore-shell-command-0cf55df6ca4b4c55.yaml | 7 | ||||
-rw-r--r-- | requirements.txt | 2 | ||||
-rw-r--r-- | test-requirements.txt | 2 | ||||
-rwxr-xr-x | tools/lintstack.py | 2 | ||||
-rw-r--r-- | tox.ini | 3 |
8 files changed, 237 insertions, 6 deletions
diff --git a/cinderclient/api_versions.py b/cinderclient/api_versions.py index ff4cc93..55dbfec 100644 --- a/cinderclient/api_versions.py +++ b/cinderclient/api_versions.py @@ -29,7 +29,7 @@ LOG = logging.getLogger(__name__) # key is a deprecated version and value is an alternative version. DEPRECATED_VERSIONS = {"2": "3"} DEPRECATED_VERSION = "2.0" -MAX_VERSION = "3.62" +MAX_VERSION = "3.63" MIN_VERSION = "3.0" _SUBSTITUTIONS = {} diff --git a/cinderclient/tests/unit/v3/test_shell.py b/cinderclient/tests/unit/v3/test_shell.py index 054f332..82e3943 100644 --- a/cinderclient/tests/unit/v3/test_shell.py +++ b/cinderclient/tests/unit/v3/test_shell.py @@ -1636,3 +1636,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 fd78c57..6d7d1eb 100644 --- a/cinderclient/v3/shell.py +++ b/cinderclient/v3/shell.py @@ -217,6 +217,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.') diff --git a/releasenotes/notes/enhance-backup-restore-shell-command-0cf55df6ca4b4c55.yaml b/releasenotes/notes/enhance-backup-restore-shell-command-0cf55df6ca4b4c55.yaml new file mode 100644 index 0000000..ee5d852 --- /dev/null +++ b/releasenotes/notes/enhance-backup-restore-shell-command-0cf55df6ca4b4c55.yaml @@ -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. diff --git a/requirements.txt b/requirements.txt index 16cf67d..1a5d13d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr>=5.5.0 # Apache-2.0 -PrettyTable<0.8,>=0.7.2 # BSD +PrettyTable>=0.7.2 # BSD keystoneauth1>=4.2.1 # Apache-2.0 simplejson>=3.5.1 # MIT oslo.i18n>=5.0.1 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 29ab6b0..c660259 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,7 +3,7 @@ # process, which may cause wedges in the gate later. # Hacking already pins down pep8, pyflakes and flake8 -hacking>=3.1.0,<3.2.0 # Apache-2.0 +hacking>=4.0.0,<4.1.0 # Apache-2.0 docutils>=0.15.2 coverage>=5.2.1 # Apache-2.0 ddt>=1.4.1 # MIT diff --git a/tools/lintstack.py b/tools/lintstack.py index 1f7923a..e0cfc28 100755 --- a/tools/lintstack.py +++ b/tools/lintstack.py @@ -153,7 +153,7 @@ def run_pylint(): args = [ "--msg-template='{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}'", "-E", "cinderclient"] - lint.Run(args, reporter=reporter, exit=False) + lint.Run(args, reporter=reporter, do_exit=False) val = buff.getvalue() buff.close() return val @@ -11,7 +11,6 @@ ignore_basepython_conflict=true [testenv] basepython = python3 usedevelop = True -install_command = pip install {opts} {packages} setenv = VIRTUAL_ENV={envdir} OS_TEST_PATH=./cinderclient/tests/unit @@ -38,7 +37,7 @@ commands = deps = -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt - pylint==1.9.1 + pylint==2.6.0 commands = bash tools/lintstack.sh whitelist_externals = bash |