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:
@@ -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.
|
@@ -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()))
|
||||
|
||||
|
@@ -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):
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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):
|
||||
|
Reference in New Issue
Block a user