Support to restore backup from data location

This feature needs to bump python-troveclient major version as it
introduced an incompatible change for backup creation CLI.

Change-Id: I6fe94ccb552e2c0020150494ccc2ba6361184229
This commit is contained in:
Lingxian Kong
2021-02-20 09:51:57 +13:00
parent 4c71809fad
commit c7319d8fe8
5 changed files with 130 additions and 36 deletions

View File

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

View File

@@ -216,16 +216,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>',
@@ -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()))

View File

@@ -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):

View File

@@ -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

View File

@@ -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):