summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cinderclient/api_versions.py2
-rw-r--r--cinderclient/tests/unit/v3/test_shell.py157
-rw-r--r--cinderclient/v3/shell.py68
-rw-r--r--releasenotes/notes/enhance-backup-restore-shell-command-0cf55df6ca4b4c55.yaml7
-rw-r--r--requirements.txt2
-rw-r--r--test-requirements.txt2
-rwxr-xr-xtools/lintstack.py2
-rw-r--r--tox.ini3
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
diff --git a/tox.ini b/tox.ini
index bd8a2c6..57dfe3b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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