Skip to content

Commit c7319d8

Browse files
committed
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
1 parent 4c71809 commit c7319d8

5 files changed

Lines changed: 130 additions & 36 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
features:
3+
- In multi-region deployment with geo-replicated Swift, the user can
4+
restore a backup in one region by manually specifying the original backup
5+
data location created in another region. Instance ID or name is not needed
6+
anymore for creating backups.

troveclient/osc/v1/database_backups.py

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -216,16 +216,18 @@ class CreateDatabaseBackup(command.ShowOne):
216216

217217
def get_parser(self, prog_name):
218218
parser = super(CreateDatabaseBackup, self).get_parser(prog_name)
219-
parser.add_argument(
220-
'instance',
221-
metavar='<instance>',
222-
help=_('ID or name of the instance.')
223-
)
224219
parser.add_argument(
225220
'name',
226221
metavar='<name>',
227222
help=_('Name of the backup.')
228223
)
224+
parser.add_argument(
225+
'-i',
226+
'--instance',
227+
metavar='<instance>',
228+
help=_('ID or name of the instance. This is not required if '
229+
'restoring a backup from the data location.')
230+
)
229231
parser.add_argument(
230232
'--description',
231233
metavar='<description>',
@@ -256,21 +258,50 @@ def get_parser(self, prog_name):
256258
'operator. Non-existent container is created '
257259
'automatically.')
258260
)
261+
parser.add_argument(
262+
'--restore-from',
263+
help=_('The original backup data location, typically this is a '
264+
'Swift object URL.')
265+
)
266+
parser.add_argument(
267+
'--restore-datastore-version',
268+
help=_('ID of the local datastore version corresponding to the '
269+
'original backup')
270+
)
271+
parser.add_argument(
272+
'--restore-size', type=float,
273+
help=_('The original backup size.')
274+
)
259275
return parser
260276

261277
def take_action(self, parsed_args):
262278
manager = self.app.client_manager.database
263279
database_backups = manager.backups
264-
instance = osc_utils.find_resource(manager.instances,
265-
parsed_args.instance)
266-
backup = database_backups.create(
267-
parsed_args.name,
268-
instance,
269-
description=parsed_args.description,
270-
parent_id=parsed_args.parent,
271-
incremental=parsed_args.incremental,
272-
swift_container=parsed_args.swift_container
273-
)
280+
params = {}
281+
instance_id = None
282+
283+
if parsed_args.restore_from:
284+
# Leave the input validation to Trove server.
285+
params.update({
286+
'restore_from': parsed_args.restore_from,
287+
'restore_ds_version': parsed_args.restore_datastore_version,
288+
'restore_size': parsed_args.restore_size,
289+
})
290+
elif not parsed_args.instance:
291+
raise exceptions.CommandError('Instance ID or name is required if '
292+
'not restoring a backup.')
293+
else:
294+
instance_id = trove_utils.get_resource_id(manager.instances,
295+
parsed_args.instance)
296+
params.update({
297+
'description': parsed_args.description,
298+
'parent_id': parsed_args.parent,
299+
'incremental': parsed_args.incremental,
300+
'swift_container': parsed_args.swift_container
301+
})
302+
303+
backup = database_backups.create(parsed_args.name, instance_id,
304+
**params)
274305
backup = set_attributes_for_print_detail(backup)
275306
return zip(*sorted(backup.items()))
276307

troveclient/tests/osc/v1/test_database_backups.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -247,39 +247,67 @@ def setUp(self):
247247
)
248248

249249
def test_backup_create_return_value(self):
250-
args = ['1234', 'bk-1234']
250+
args = ['bk-1234', '--instance', self.random_uuid()]
251251
parsed_args = self.check_parser(self.cmd, args, [])
252252
columns, data = self.cmd.take_action(parsed_args)
253253
self.assertEqual(self.columns, columns)
254254
self.assertEqual(self.values, data)
255255

256-
@mock.patch.object(utils, 'find_resource')
256+
@mock.patch('troveclient.utils.get_resource_id_by_name')
257257
def test_backup_create(self, mock_find):
258-
args = ['1234', 'bk-1234-1']
259-
mock_find.return_value = args[0]
258+
args = ['bk-1234-1', '--instance', '1234']
259+
mock_find.return_value = 'fake-instance-id'
260260
parsed_args = self.check_parser(self.cmd, args, [])
261261
self.cmd.take_action(parsed_args)
262262
self.backup_client.create.assert_called_with('bk-1234-1',
263-
'1234',
263+
'fake-instance-id',
264264
description=None,
265265
parent_id=None,
266266
incremental=False,
267267
swift_container=None)
268268

269-
@mock.patch.object(utils, 'find_resource')
269+
@mock.patch('troveclient.utils.get_resource_id_by_name')
270270
def test_incremental_backup_create(self, mock_find):
271-
args = ['1234', 'bk-1234-2', '--description', 'backup 1234',
272-
'--parent', '1234-1', '--incremental']
273-
mock_find.return_value = args[0]
271+
args = ['bk-1234-2', '--instance', '1234', '--description',
272+
'backup 1234', '--parent', '1234-1', '--incremental']
273+
mock_find.return_value = 'fake-instance-id'
274+
274275
parsed_args = self.check_parser(self.cmd, args, [])
275276
self.cmd.take_action(parsed_args)
277+
276278
self.backup_client.create.assert_called_with('bk-1234-2',
277-
'1234',
279+
'fake-instance-id',
278280
description='backup 1234',
279281
parent_id='1234-1',
280282
incremental=True,
281283
swift_container=None)
282284

285+
def test_create_from_data_location(self):
286+
name = self.random_name('backup')
287+
ds_version = self.random_uuid()
288+
args = [name, '--restore-from', 'fake-remote-location',
289+
'--restore-datastore-version', ds_version, '--restore-size',
290+
'3']
291+
parsed_args = self.check_parser(self.cmd, args, [])
292+
293+
self.cmd.take_action(parsed_args)
294+
295+
self.backup_client.create.assert_called_with(
296+
name,
297+
None,
298+
restore_from='fake-remote-location',
299+
restore_ds_version=ds_version,
300+
restore_size=3,
301+
)
302+
303+
def test_required_params_missing(self):
304+
args = [self.random_name('backup')]
305+
parsed_args = self.check_parser(self.cmd, args, [])
306+
self.assertRaises(
307+
exceptions.CommandError,
308+
self.cmd.take_action,
309+
parsed_args)
310+
283311

284312
class TestDatabaseBackupExecutionDelete(TestBackups):
285313

troveclient/utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import uuid
2222

2323
from oslo_utils import encodeutils
24+
from oslo_utils import uuidutils
2425
import prettytable
2526

2627
from troveclient.apiclient import exceptions
@@ -207,6 +208,18 @@ def print_dict(d, property="Property"):
207208
_print(pt, property)
208209

209210

211+
def get_resource_id(manager, id_or_name):
212+
if not uuidutils.is_uuid_like(id_or_name):
213+
try:
214+
id_or_name = get_resource_id_by_name(manager, id_or_name)
215+
except Exception as e:
216+
msg = ("Failed to get resource ID for %s, error: %s" %
217+
(id_or_name, str(e)))
218+
raise exceptions.CommandError(msg)
219+
220+
return id_or_name
221+
222+
210223
def get_resource_id_by_name(manager, name):
211224
resource = manager.find(name=name)
212225
return resource.id

troveclient/v1/backups.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,9 @@ def list(self, limit=None, marker=None, datastore=None, instance_id=None,
7575
query_strings)
7676

7777
def create(self, name, instance, description=None,
78-
parent_id=None, incremental=False, swift_container=None):
79-
"""Create a new backup from the given instance.
78+
parent_id=None, incremental=False, swift_container=None,
79+
restore_from=None, restore_ds_version=None, restore_size=None):
80+
"""Create or restore a new backup.
8081
8182
:param name: name for backup.
8283
:param instance: instance to backup.
@@ -85,23 +86,38 @@ def create(self, name, instance, description=None,
8586
:param incremental: flag to indicate incremental backup based on
8687
last backup
8788
:param swift_container: Swift container name.
89+
:param restore_from: The original backup data location, typically this
90+
is a Swift object URL.
91+
:param restore_ds_version: ID of the local datastore version
92+
corresponding to the original backup.
93+
:param restore_size: The original backup size.
8894
:returns: :class:`Backups`
8995
"""
9096
body = {
9197
"backup": {
9298
"name": name,
93-
"incremental": int(incremental)
9499
}
95100
}
96101

97-
if instance:
98-
body['backup']['instance'] = base.getid(instance)
99-
if description:
100-
body['backup']['description'] = description
101-
if parent_id:
102-
body['backup']['parent_id'] = parent_id
103-
if swift_container:
104-
body['backup']['swift_container'] = swift_container
102+
if restore_from:
103+
body['backup'].update({
104+
'restore_from': {
105+
'remote_location': restore_from,
106+
'local_datastore_version_id': restore_ds_version,
107+
'size': restore_size
108+
}
109+
})
110+
else:
111+
body['backup']['incremental'] = int(incremental)
112+
if instance:
113+
body['backup']['instance'] = base.getid(instance)
114+
if description:
115+
body['backup']['description'] = description
116+
if parent_id:
117+
body['backup']['parent_id'] = parent_id
118+
if swift_container:
119+
body['backup']['swift_container'] = swift_container
120+
105121
return self._create("/backups", body, "backup")
106122

107123
def delete(self, backup):

0 commit comments

Comments
 (0)