diff options
-rw-r--r-- | releasenotes/notes/wallaby-restore-backup-from-remote.yaml | 6 | ||||
-rw-r--r-- | troveclient/osc/v1/database_backups.py | 61 | ||||
-rw-r--r-- | troveclient/tests/osc/v1/test_database_backups.py | 48 | ||||
-rw-r--r-- | troveclient/utils.py | 13 | ||||
-rw-r--r-- | troveclient/v1/backups.py | 38 |
5 files changed, 130 insertions, 36 deletions
diff --git a/releasenotes/notes/wallaby-restore-backup-from-remote.yaml b/releasenotes/notes/wallaby-restore-backup-from-remote.yaml new file mode 100644 index 0000000..9b8f389 --- /dev/null +++ b/releasenotes/notes/wallaby-restore-backup-from-remote.yaml @@ -0,0 +1,6 @@ +--- +features: + - In multi-region deployment with geo-replicated Swift, the user can + restore a backup in one region by manually specifying the original backup + data location created in another region. Instance ID or name is not needed + anymore for creating backups. diff --git a/troveclient/osc/v1/database_backups.py b/troveclient/osc/v1/database_backups.py index f4dd0e2..28ceea7 100644 --- a/troveclient/osc/v1/database_backups.py +++ b/troveclient/osc/v1/database_backups.py @@ -217,16 +217,18 @@ class CreateDatabaseBackup(command.ShowOne): def get_parser(self, prog_name): parser = super(CreateDatabaseBackup, self).get_parser(prog_name) parser.add_argument( - 'instance', - metavar='<instance>', - help=_('ID or name of the instance.') - ) - parser.add_argument( 'name', metavar='<name>', help=_('Name of the backup.') ) parser.add_argument( + '-i', + '--instance', + metavar='<instance>', + help=_('ID or name of the instance. This is not required if ' + 'restoring a backup from the data location.') + ) + parser.add_argument( '--description', metavar='<description>', default=None, @@ -256,21 +258,50 @@ class CreateDatabaseBackup(command.ShowOne): 'operator. Non-existent container is created ' 'automatically.') ) + parser.add_argument( + '--restore-from', + help=_('The original backup data location, typically this is a ' + 'Swift object URL.') + ) + parser.add_argument( + '--restore-datastore-version', + help=_('ID of the local datastore version corresponding to the ' + 'original backup') + ) + parser.add_argument( + '--restore-size', type=float, + help=_('The original backup size.') + ) return parser def take_action(self, parsed_args): manager = self.app.client_manager.database database_backups = manager.backups - instance = osc_utils.find_resource(manager.instances, - parsed_args.instance) - backup = database_backups.create( - parsed_args.name, - instance, - description=parsed_args.description, - parent_id=parsed_args.parent, - incremental=parsed_args.incremental, - swift_container=parsed_args.swift_container - ) + params = {} + instance_id = None + + if parsed_args.restore_from: + # Leave the input validation to Trove server. + params.update({ + 'restore_from': parsed_args.restore_from, + 'restore_ds_version': parsed_args.restore_datastore_version, + 'restore_size': parsed_args.restore_size, + }) + elif not parsed_args.instance: + raise exceptions.CommandError('Instance ID or name is required if ' + 'not restoring a backup.') + else: + instance_id = trove_utils.get_resource_id(manager.instances, + parsed_args.instance) + params.update({ + 'description': parsed_args.description, + 'parent_id': parsed_args.parent, + 'incremental': parsed_args.incremental, + 'swift_container': parsed_args.swift_container + }) + + backup = database_backups.create(parsed_args.name, instance_id, + **params) backup = set_attributes_for_print_detail(backup) return zip(*sorted(backup.items())) diff --git a/troveclient/tests/osc/v1/test_database_backups.py b/troveclient/tests/osc/v1/test_database_backups.py index a23622f..bc67d9d 100644 --- a/troveclient/tests/osc/v1/test_database_backups.py +++ b/troveclient/tests/osc/v1/test_database_backups.py @@ -247,39 +247,67 @@ class TestBackupCreate(TestBackups): ) def test_backup_create_return_value(self): - args = ['1234', 'bk-1234'] + args = ['bk-1234', '--instance', self.random_uuid()] parsed_args = self.check_parser(self.cmd, args, []) columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) self.assertEqual(self.values, data) - @mock.patch.object(utils, 'find_resource') + @mock.patch('troveclient.utils.get_resource_id_by_name') def test_backup_create(self, mock_find): - args = ['1234', 'bk-1234-1'] - mock_find.return_value = args[0] + args = ['bk-1234-1', '--instance', '1234'] + mock_find.return_value = 'fake-instance-id' parsed_args = self.check_parser(self.cmd, args, []) self.cmd.take_action(parsed_args) self.backup_client.create.assert_called_with('bk-1234-1', - '1234', + 'fake-instance-id', description=None, parent_id=None, incremental=False, swift_container=None) - @mock.patch.object(utils, 'find_resource') + @mock.patch('troveclient.utils.get_resource_id_by_name') def test_incremental_backup_create(self, mock_find): - args = ['1234', 'bk-1234-2', '--description', 'backup 1234', - '--parent', '1234-1', '--incremental'] - mock_find.return_value = args[0] + args = ['bk-1234-2', '--instance', '1234', '--description', + 'backup 1234', '--parent', '1234-1', '--incremental'] + mock_find.return_value = 'fake-instance-id' + parsed_args = self.check_parser(self.cmd, args, []) self.cmd.take_action(parsed_args) + self.backup_client.create.assert_called_with('bk-1234-2', - '1234', + 'fake-instance-id', description='backup 1234', parent_id='1234-1', incremental=True, swift_container=None) + def test_create_from_data_location(self): + name = self.random_name('backup') + ds_version = self.random_uuid() + args = [name, '--restore-from', 'fake-remote-location', + '--restore-datastore-version', ds_version, '--restore-size', + '3'] + parsed_args = self.check_parser(self.cmd, args, []) + + self.cmd.take_action(parsed_args) + + self.backup_client.create.assert_called_with( + name, + None, + restore_from='fake-remote-location', + restore_ds_version=ds_version, + restore_size=3, + ) + + def test_required_params_missing(self): + args = [self.random_name('backup')] + parsed_args = self.check_parser(self.cmd, args, []) + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + class TestDatabaseBackupExecutionDelete(TestBackups): diff --git a/troveclient/utils.py b/troveclient/utils.py index 2c6f809..28a2537 100644 --- a/troveclient/utils.py +++ b/troveclient/utils.py @@ -21,6 +21,7 @@ import sys import uuid from oslo_utils import encodeutils +from oslo_utils import uuidutils import prettytable from troveclient.apiclient import exceptions @@ -207,6 +208,18 @@ def print_dict(d, property="Property"): _print(pt, property) +def get_resource_id(manager, id_or_name): + if not uuidutils.is_uuid_like(id_or_name): + try: + id_or_name = get_resource_id_by_name(manager, id_or_name) + except Exception as e: + msg = ("Failed to get resource ID for %s, error: %s" % + (id_or_name, str(e))) + raise exceptions.CommandError(msg) + + return id_or_name + + def get_resource_id_by_name(manager, name): resource = manager.find(name=name) return resource.id diff --git a/troveclient/v1/backups.py b/troveclient/v1/backups.py index 4c5498e..4e309dd 100644 --- a/troveclient/v1/backups.py +++ b/troveclient/v1/backups.py @@ -75,8 +75,9 @@ class Backups(base.ManagerWithFind): query_strings) def create(self, name, instance, description=None, - parent_id=None, incremental=False, swift_container=None): - """Create a new backup from the given instance. + parent_id=None, incremental=False, swift_container=None, + restore_from=None, restore_ds_version=None, restore_size=None): + """Create or restore a new backup. :param name: name for backup. :param instance: instance to backup. @@ -85,23 +86,38 @@ class Backups(base.ManagerWithFind): :param incremental: flag to indicate incremental backup based on last backup :param swift_container: Swift container name. + :param restore_from: The original backup data location, typically this + is a Swift object URL. + :param restore_ds_version: ID of the local datastore version + corresponding to the original backup. + :param restore_size: The original backup size. :returns: :class:`Backups` """ body = { "backup": { "name": name, - "incremental": int(incremental) } } - if instance: - body['backup']['instance'] = base.getid(instance) - if description: - body['backup']['description'] = description - if parent_id: - body['backup']['parent_id'] = parent_id - if swift_container: - body['backup']['swift_container'] = swift_container + if restore_from: + body['backup'].update({ + 'restore_from': { + 'remote_location': restore_from, + 'local_datastore_version_id': restore_ds_version, + 'size': restore_size + } + }) + else: + body['backup']['incremental'] = int(incremental) + if instance: + body['backup']['instance'] = base.getid(instance) + if description: + body['backup']['description'] = description + if parent_id: + body['backup']['parent_id'] = parent_id + if swift_container: + body['backup']['swift_container'] = swift_container + return self._create("/backups", body, "backup") def delete(self, backup): |