summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLingxian Kong <anlin.kong@gmail.com>2021-02-20 09:51:57 +1300
committerLingxian Kong <anlin.kong@gmail.com>2021-02-21 00:37:08 +1300
commitc7319d8fe8cb6cfd5c423472283aadffb12ed52c (patch)
tree77e269a79927c26cd084f6cdf7e13296d620f0a8
parent4c71809fadc070aa0f1ec3b4082546d4d2cf0d51 (diff)
downloadpython-troveclient-c7319d8fe8cb6cfd5c423472283aadffb12ed52c.tar.gz
Support to restore backup from data locationwallaby-em7.0.0
This feature needs to bump python-troveclient major version as it introduced an incompatible change for backup creation CLI. Change-Id: I6fe94ccb552e2c0020150494ccc2ba6361184229
-rw-r--r--releasenotes/notes/wallaby-restore-backup-from-remote.yaml6
-rw-r--r--troveclient/osc/v1/database_backups.py61
-rw-r--r--troveclient/tests/osc/v1/test_database_backups.py48
-rw-r--r--troveclient/utils.py13
-rw-r--r--troveclient/v1/backups.py38
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):