Support to restore backup from remote location
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. Change-Id: Iefef3bf969163af707935445bc23299400dc88c3
This commit is contained in:
parent
9c2e0bf3a0
commit
6fdf11ea7f
@ -76,6 +76,19 @@ In the Trove deployment with service tenant enabled, The backup data is
|
|||||||
stored as objects in OpenStack Swift service in the user's container. If not
|
stored as objects in OpenStack Swift service in the user's container. If not
|
||||||
specified, the container name is defined by the cloud admin.
|
specified, the container name is defined by the cloud admin.
|
||||||
|
|
||||||
|
The user can create a backup strategy within the project scope or specific to
|
||||||
|
a particular instance.
|
||||||
|
|
||||||
|
In multi-region deployment with geo-replicated Swift, the user can also restore
|
||||||
|
a backup in a region by manually specifying the backup data location created in
|
||||||
|
another region, then create instances from the backup. Instance ID is not
|
||||||
|
required in this case.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
The restored backup is dependent on the original backup data, if the
|
||||||
|
original backup is deleted, the restored backup is invalid.
|
||||||
|
|
||||||
Normal response codes: 202
|
Normal response codes: 202
|
||||||
|
|
||||||
Request
|
Request
|
||||||
@ -90,6 +103,7 @@ Request
|
|||||||
- incremental: backup_incremental
|
- incremental: backup_incremental
|
||||||
- description: backup_description
|
- description: backup_description
|
||||||
- swift_container: swift_container
|
- swift_container: swift_container
|
||||||
|
- restore_from: backup_restore_from
|
||||||
|
|
||||||
Request Example
|
Request Example
|
||||||
---------------
|
---------------
|
||||||
|
@ -148,7 +148,7 @@ backup_instanceId:
|
|||||||
description: |
|
description: |
|
||||||
The ID of the instance to create backup for.
|
The ID of the instance to create backup for.
|
||||||
in: body
|
in: body
|
||||||
required: true
|
required: false
|
||||||
type: string
|
type: string
|
||||||
backup_list:
|
backup_list:
|
||||||
description: |
|
description: |
|
||||||
@ -180,6 +180,17 @@ backup_parentId1:
|
|||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
backup_restore_from:
|
||||||
|
description: |
|
||||||
|
The information needed to restore a backup, including:
|
||||||
|
|
||||||
|
- ``remote_location``: The original backup data location.
|
||||||
|
- ``local_datastore_version_id``: The local datastore version corresponding
|
||||||
|
to the original backup.
|
||||||
|
- ``size``: The original backup size.
|
||||||
|
in: body
|
||||||
|
required: false
|
||||||
|
type: object
|
||||||
backup_size:
|
backup_size:
|
||||||
description: |
|
description: |
|
||||||
Size of the backup, the unit is GB.
|
Size of the backup, the unit is GB.
|
||||||
|
@ -276,3 +276,21 @@ Create an incremental backup based on a parent backup:
|
|||||||
| status | NEW |
|
| status | NEW |
|
||||||
| updated | 2014-03-19T14:09:13 |
|
| updated | 2014-03-19T14:09:13 |
|
||||||
+-------------+--------------------------------------+
|
+-------------+--------------------------------------+
|
||||||
|
|
||||||
|
Restore backup from other regions
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
Restoring backup from other regions were introduced in Wallaby,
|
||||||
|
|
||||||
|
In multi-region deployment with geo-replicated Swift, the user is able to
|
||||||
|
create a backup in one region using the backup data created in the others,
|
||||||
|
which is useful in Disaster Recovery scenario. Instance ID is not required in
|
||||||
|
this case when restoring backup, but the original backup data location (a swift
|
||||||
|
object URL), the local datastore version and the backup data size are required.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
The restored backup is dependent on the original backup data, if the
|
||||||
|
original backup is deleted, the restored backup is invalid.
|
||||||
|
|
||||||
|
TODO: Add CLI example once supported in python-troveclient.
|
||||||
|
5
releasenotes/notes/wallaby-restore-backup.yaml
Normal file
5
releasenotes/notes/wallaby-restore-backup.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
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.
|
@ -22,7 +22,9 @@ from swiftclient.client import ClientException
|
|||||||
from trove.backup.state import BackupState
|
from trove.backup.state import BackupState
|
||||||
from trove.common import cfg
|
from trove.common import cfg
|
||||||
from trove.common import clients
|
from trove.common import clients
|
||||||
|
from trove.common import constants
|
||||||
from trove.common import exception
|
from trove.common import exception
|
||||||
|
from trove.common import swift
|
||||||
from trove.common import utils
|
from trove.common import utils
|
||||||
from trove.common.i18n import _
|
from trove.common.i18n import _
|
||||||
from trove.datastore import models as datastore_models
|
from trove.datastore import models as datastore_models
|
||||||
@ -49,7 +51,8 @@ class Backup(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, context, instance, name, description=None,
|
def create(cls, context, instance, name, description=None,
|
||||||
parent_id=None, incremental=False, swift_container=None):
|
parent_id=None, incremental=False, swift_container=None,
|
||||||
|
restore_from=None):
|
||||||
"""
|
"""
|
||||||
create db record for Backup
|
create db record for Backup
|
||||||
:param cls:
|
:param cls:
|
||||||
@ -61,29 +64,59 @@ class Backup(object):
|
|||||||
:param incremental: flag to indicate incremental backup
|
:param incremental: flag to indicate incremental backup
|
||||||
based on previous backup
|
based on previous backup
|
||||||
:param swift_container: Swift container name.
|
:param swift_container: Swift container name.
|
||||||
|
:param restore_from: A dict that contains backup information of another
|
||||||
|
region.
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
backup_state = BackupState.NEW
|
||||||
|
checksum = None
|
||||||
|
instance_id = None
|
||||||
|
parent = None
|
||||||
|
last_backup_id = None
|
||||||
|
location = None
|
||||||
|
backup_type = constants.BACKUP_TYPE_FULL
|
||||||
|
size = None
|
||||||
|
|
||||||
def _create_resources():
|
if restore_from:
|
||||||
# parse the ID from the Ref
|
# Check location and datastore version.
|
||||||
|
LOG.info(f"Restoring backup, restore_from: {restore_from}")
|
||||||
|
backup_state = BackupState.RESTORED
|
||||||
|
|
||||||
|
ds_version_id = restore_from.get('local_datastore_version_id')
|
||||||
|
ds_version = datastore_models.DatastoreVersion.load_by_uuid(
|
||||||
|
ds_version_id)
|
||||||
|
|
||||||
|
location = restore_from.get('remote_location')
|
||||||
|
swift_client = clients.create_swift_client(context)
|
||||||
|
try:
|
||||||
|
obj_meta = swift.get_metadata(swift_client, location,
|
||||||
|
extra_attrs=['etag'])
|
||||||
|
except Exception:
|
||||||
|
msg = f'Failed to restore backup from {location}'
|
||||||
|
LOG.exception(msg)
|
||||||
|
raise exception.BackupCreationError(msg)
|
||||||
|
|
||||||
|
checksum = obj_meta['etag']
|
||||||
|
if 'parent_location' in obj_meta:
|
||||||
|
backup_type = constants.BACKUP_TYPE_INC
|
||||||
|
|
||||||
|
size = restore_from['size']
|
||||||
|
else:
|
||||||
instance_id = utils.get_id_from_href(instance)
|
instance_id = utils.get_id_from_href(instance)
|
||||||
|
# Import here to avoid circular imports.
|
||||||
# verify that the instance exists and can perform actions
|
from trove.instance import models as inst_model
|
||||||
from trove.instance.models import Instance
|
instance_model = inst_model.Instance.load(context, instance_id)
|
||||||
instance_model = Instance.load(context, instance_id)
|
|
||||||
instance_model.validate_can_perform_action()
|
instance_model.validate_can_perform_action()
|
||||||
cls.validate_can_perform_action(
|
|
||||||
instance_model, 'backup_create')
|
|
||||||
|
|
||||||
cls.verify_swift_auth_token(context)
|
|
||||||
|
|
||||||
if instance_model.cluster_id is not None:
|
if instance_model.cluster_id is not None:
|
||||||
raise exception.ClusterInstanceOperationNotSupported()
|
raise exception.ClusterInstanceOperationNotSupported()
|
||||||
|
|
||||||
|
cls.validate_can_perform_action(instance_model, 'backup_create')
|
||||||
|
|
||||||
|
cls.verify_swift_auth_token(context)
|
||||||
|
|
||||||
ds = instance_model.datastore
|
ds = instance_model.datastore
|
||||||
ds_version = instance_model.datastore_version
|
ds_version = instance_model.datastore_version
|
||||||
parent = None
|
|
||||||
last_backup_id = None
|
|
||||||
if parent_id:
|
if parent_id:
|
||||||
# Look up the parent info or fail early if not found or if
|
# Look up the parent info or fail early if not found or if
|
||||||
# the user does not have access to the parent.
|
# the user does not have access to the parent.
|
||||||
@ -100,22 +133,34 @@ class Backup(object):
|
|||||||
'checksum': _parent.checksum
|
'checksum': _parent.checksum
|
||||||
}
|
}
|
||||||
last_backup_id = _parent.id
|
last_backup_id = _parent.id
|
||||||
|
|
||||||
|
if parent:
|
||||||
|
backup_type = constants.BACKUP_TYPE_INC
|
||||||
|
|
||||||
|
def _create_resources():
|
||||||
try:
|
try:
|
||||||
db_info = DBBackup.create(name=name,
|
db_info = DBBackup.create(
|
||||||
|
name=name,
|
||||||
description=description,
|
description=description,
|
||||||
tenant_id=context.project_id,
|
tenant_id=context.project_id,
|
||||||
state=BackupState.NEW,
|
state=backup_state,
|
||||||
instance_id=instance_id,
|
instance_id=instance_id,
|
||||||
parent_id=parent_id or
|
parent_id=parent_id or last_backup_id,
|
||||||
last_backup_id,
|
|
||||||
datastore_version_id=ds_version.id,
|
datastore_version_id=ds_version.id,
|
||||||
deleted=False)
|
deleted=False,
|
||||||
|
location=location,
|
||||||
|
checksum=checksum,
|
||||||
|
backup_type=backup_type,
|
||||||
|
size=size
|
||||||
|
)
|
||||||
except exception.InvalidModelError as ex:
|
except exception.InvalidModelError as ex:
|
||||||
LOG.exception("Unable to create backup record for "
|
LOG.exception("Unable to create backup record for "
|
||||||
"instance: %s", instance_id)
|
"instance: %s", instance_id)
|
||||||
raise exception.BackupCreationError(str(ex))
|
raise exception.BackupCreationError(str(ex))
|
||||||
|
|
||||||
backup_info = {'id': db_info.id,
|
if not restore_from:
|
||||||
|
backup_info = {
|
||||||
|
'id': db_info.id,
|
||||||
'name': name,
|
'name': name,
|
||||||
'description': description,
|
'description': description,
|
||||||
'instance_id': instance_id,
|
'instance_id': instance_id,
|
||||||
@ -127,9 +172,14 @@ class Backup(object):
|
|||||||
'swift_container': swift_container
|
'swift_container': swift_container
|
||||||
}
|
}
|
||||||
api.API(context).create_backup(backup_info, instance_id)
|
api.API(context).create_backup(backup_info, instance_id)
|
||||||
|
else:
|
||||||
|
context.notification.payload.update(
|
||||||
|
{'backup_id': db_info.id}
|
||||||
|
)
|
||||||
|
|
||||||
return db_info
|
return db_info
|
||||||
return run_with_quotas(context.project_id,
|
|
||||||
{'backups': 1},
|
return run_with_quotas(context.project_id, {'backups': 1},
|
||||||
_create_resources)
|
_create_resources)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -372,7 +422,7 @@ class DBBackup(DatabaseModelBase):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_done_successfuly(self):
|
def is_done_successfuly(self):
|
||||||
return self.state == BackupState.COMPLETED
|
return self.state in [BackupState.COMPLETED, BackupState.RESTORED]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def filename(self):
|
def filename(self):
|
||||||
|
@ -80,16 +80,20 @@ class BackupController(wsgi.Controller):
|
|||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
policy.authorize_on_tenant(context, 'backup:create')
|
policy.authorize_on_tenant(context, 'backup:create')
|
||||||
data = body['backup']
|
data = body['backup']
|
||||||
instance = data['instance']
|
instance = data.get('instance')
|
||||||
name = data['name']
|
name = data['name']
|
||||||
desc = data.get('description')
|
desc = data.get('description')
|
||||||
parent = data.get('parent_id')
|
parent = data.get('parent_id')
|
||||||
incremental = data.get('incremental')
|
incremental = data.get('incremental')
|
||||||
swift_container = data.get('swift_container')
|
swift_container = data.get('swift_container')
|
||||||
|
restore_from = data.get('restore_from')
|
||||||
|
|
||||||
context.notification = notification.DBaaSBackupCreate(context,
|
context.notification = notification.DBaaSBackupCreate(
|
||||||
request=req)
|
context, request=req)
|
||||||
|
|
||||||
|
if not restore_from:
|
||||||
|
if not instance:
|
||||||
|
raise exception.BackupCreationError('instance is missing.')
|
||||||
if not swift_container:
|
if not swift_container:
|
||||||
instance_id = utils.get_id_from_href(instance)
|
instance_id = utils.get_id_from_href(instance)
|
||||||
backup_strategy = BackupStrategy.get(context, instance_id)
|
backup_strategy = BackupStrategy.get(context, instance_id)
|
||||||
@ -100,7 +104,8 @@ class BackupController(wsgi.Controller):
|
|||||||
description=desc, parent_id=parent):
|
description=desc, parent_id=parent):
|
||||||
backup = Backup.create(context, instance, name, desc,
|
backup = Backup.create(context, instance, name, desc,
|
||||||
parent_id=parent, incremental=incremental,
|
parent_id=parent, incremental=incremental,
|
||||||
swift_container=swift_container)
|
swift_container=swift_container,
|
||||||
|
restore_from=restore_from)
|
||||||
|
|
||||||
return wsgi.Result(views.BackupView(backup).data(), 202)
|
return wsgi.Result(views.BackupView(backup).data(), 202)
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ class BackupState(object):
|
|||||||
SAVING = "SAVING"
|
SAVING = "SAVING"
|
||||||
COMPLETED = "COMPLETED"
|
COMPLETED = "COMPLETED"
|
||||||
FAILED = "FAILED"
|
FAILED = "FAILED"
|
||||||
|
RESTORED = "RESTORED"
|
||||||
DELETE_FAILED = "DELETE_FAILED"
|
DELETE_FAILED = "DELETE_FAILED"
|
||||||
RUNNING_STATES = [NEW, BUILDING, SAVING]
|
RUNNING_STATES = [NEW, BUILDING, SAVING]
|
||||||
END_STATES = [COMPLETED, FAILED, DELETE_FAILED]
|
END_STATES = [COMPLETED, FAILED, DELETE_FAILED, RESTORED]
|
||||||
|
@ -652,14 +652,27 @@ backup = {
|
|||||||
"properties": {
|
"properties": {
|
||||||
"backup": {
|
"backup": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["instance", "name"],
|
"required": ["name"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"description": non_empty_string,
|
"description": non_empty_string,
|
||||||
"instance": uuid,
|
"instance": uuid,
|
||||||
"name": non_empty_string,
|
"name": non_empty_string,
|
||||||
"parent_id": uuid,
|
"parent_id": uuid,
|
||||||
"incremental": boolean_string,
|
"incremental": boolean_string,
|
||||||
"swift_container": non_empty_string
|
"swift_container": non_empty_string,
|
||||||
|
"restore_from": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"remote_location",
|
||||||
|
"local_datastore_version_id",
|
||||||
|
"size"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"remote_location": non_empty_string,
|
||||||
|
"local_datastore_version_id": uuid,
|
||||||
|
"size": {"type": "number"}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
16
trove/common/constants.py
Normal file
16
trove/common/constants.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Copyright 2021 Catalyst Cloud Ltd.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
BACKUP_TYPE_FULL = 'full'
|
||||||
|
BACKUP_TYPE_INC = 'incremental'
|
42
trove/common/swift.py
Normal file
42
trove/common/swift.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# Copyright 2021 Catalyst Cloud Ltd.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
|
def parse_location(location):
|
||||||
|
storage_url = "/".join(location.split('/')[:-2])
|
||||||
|
container_name = location.split('/')[-2]
|
||||||
|
object_name = location.split('/')[-1]
|
||||||
|
return storage_url, container_name, object_name
|
||||||
|
|
||||||
|
|
||||||
|
def _get_attr(original):
|
||||||
|
"""Get a friendly name from an object header key."""
|
||||||
|
key = original.replace('-', '_')
|
||||||
|
key = key.replace('x_object_meta_', '')
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
def get_metadata(client, location, extra_attrs=[]):
|
||||||
|
_, container_name, object_name = parse_location(location)
|
||||||
|
headers = client.head_object(container_name, object_name)
|
||||||
|
|
||||||
|
meta = {}
|
||||||
|
for key, value in headers.items():
|
||||||
|
if key.startswith('x-object-meta'):
|
||||||
|
meta[_get_attr(key)] = value
|
||||||
|
|
||||||
|
for key in extra_attrs:
|
||||||
|
meta[key] = headers.get(key)
|
||||||
|
|
||||||
|
return meta
|
@ -351,7 +351,7 @@ class SimpleInstance(object):
|
|||||||
- Then server status
|
- Then server status
|
||||||
- Otherwise, unknown
|
- Otherwise, unknown
|
||||||
"""
|
"""
|
||||||
LOG.info(f"Getting instance status for {self.id}, "
|
LOG.debug(f"Getting instance status for {self.id}, "
|
||||||
f"task status: {self.db_info.task_status}, "
|
f"task status: {self.db_info.task_status}, "
|
||||||
f"datastore status: {self.datastore_status.status}, "
|
f"datastore status: {self.datastore_status.status}, "
|
||||||
f"server status: {self.db_info.server_status}")
|
f"server status: {self.db_info.server_status}")
|
||||||
|
@ -1470,8 +1470,7 @@ class BackupTasks(object):
|
|||||||
def _delete(backup):
|
def _delete(backup):
|
||||||
backup.deleted = True
|
backup.deleted = True
|
||||||
backup.deleted_at = timeutils.utcnow()
|
backup.deleted_at = timeutils.utcnow()
|
||||||
# Set datastore_version_id to None so that datastore_version could
|
# Set datastore_version_id to None to remove dependency.
|
||||||
# be deleted.
|
|
||||||
backup.datastore_version_id = None
|
backup.datastore_version_id = None
|
||||||
backup.save()
|
backup.save()
|
||||||
|
|
||||||
@ -1479,7 +1478,9 @@ class BackupTasks(object):
|
|||||||
backup = bkup_models.Backup.get_by_id(context, backup_id)
|
backup = bkup_models.Backup.get_by_id(context, backup_id)
|
||||||
try:
|
try:
|
||||||
filename = backup.filename
|
filename = backup.filename
|
||||||
if filename:
|
# Do not remove the object if the backup was restored from remote
|
||||||
|
# location.
|
||||||
|
if filename and backup.state != bkup_models.BackupState.RESTORED:
|
||||||
BackupTasks.delete_files_from_swift(context,
|
BackupTasks.delete_files_from_swift(context,
|
||||||
backup.container_name,
|
backup.container_name,
|
||||||
filename)
|
filename)
|
||||||
|
@ -176,7 +176,7 @@ class BackupCreateTest(trove_testtools.TestCase):
|
|||||||
BACKUP_NAME, BACKUP_DESC)
|
BACKUP_NAME, BACKUP_DESC)
|
||||||
|
|
||||||
def test_create_backup_swift_token_invalid(self):
|
def test_create_backup_swift_token_invalid(self):
|
||||||
instance = MagicMock()
|
instance = MagicMock(cluster_id=None)
|
||||||
with patch.object(instance_models.BuiltInstance, 'load',
|
with patch.object(instance_models.BuiltInstance, 'load',
|
||||||
return_value=instance):
|
return_value=instance):
|
||||||
instance.validate_can_perform_action = MagicMock(
|
instance.validate_can_perform_action = MagicMock(
|
||||||
@ -191,7 +191,7 @@ class BackupCreateTest(trove_testtools.TestCase):
|
|||||||
BACKUP_NAME, BACKUP_DESC)
|
BACKUP_NAME, BACKUP_DESC)
|
||||||
|
|
||||||
def test_create_backup_datastore_operation_not_supported(self):
|
def test_create_backup_datastore_operation_not_supported(self):
|
||||||
instance = MagicMock()
|
instance = MagicMock(cluster_id=None)
|
||||||
with patch.object(instance_models.BuiltInstance, 'load',
|
with patch.object(instance_models.BuiltInstance, 'load',
|
||||||
return_value=instance):
|
return_value=instance):
|
||||||
with patch.object(
|
with patch.object(
|
||||||
|
84
trove/tests/unittests/backup/test_service.py
Normal file
84
trove/tests/unittests/backup/test_service.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
# Copyright 2021 Catalyst Cloud
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from trove.backup import service
|
||||||
|
from trove.backup.state import BackupState
|
||||||
|
from trove.common import context
|
||||||
|
from trove.common import wsgi
|
||||||
|
from trove.datastore import models as ds_models
|
||||||
|
from trove.tests.unittests import trove_testtools
|
||||||
|
from trove.tests.unittests.util import util
|
||||||
|
|
||||||
|
|
||||||
|
class TestBackupController(trove_testtools.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
util.init_db()
|
||||||
|
|
||||||
|
cls.ds_name = cls.random_name('datastore',
|
||||||
|
prefix='TestBackupController')
|
||||||
|
ds_models.update_datastore(name=cls.ds_name, default_version=None)
|
||||||
|
cls.ds = ds_models.Datastore.load(cls.ds_name)
|
||||||
|
|
||||||
|
ds_models.update_datastore_version(
|
||||||
|
cls.ds_name, 'fake-ds-version', 'mysql', '', ['trove', 'mysql'],
|
||||||
|
'', 1)
|
||||||
|
cls.ds_version = ds_models.DatastoreVersion.load(
|
||||||
|
cls.ds, 'fake-ds-version')
|
||||||
|
|
||||||
|
cls.controller = service.BackupController()
|
||||||
|
|
||||||
|
super(TestBackupController, cls).setUpClass()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
util.cleanup_db()
|
||||||
|
super(TestBackupController, cls).tearDownClass()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
trove_testtools.patch_notifier(self)
|
||||||
|
self.context = context.TroveContext(project_id=self.random_uuid())
|
||||||
|
|
||||||
|
super(TestBackupController, self).setUp()
|
||||||
|
|
||||||
|
@mock.patch('trove.common.clients.create_swift_client')
|
||||||
|
def test_create_restore_from(self, mock_swift_client):
|
||||||
|
swift_client = mock.MagicMock()
|
||||||
|
swift_client.head_object.return_value = {'etag': 'fake-etag'}
|
||||||
|
mock_swift_client.return_value = swift_client
|
||||||
|
|
||||||
|
req = mock.MagicMock(environ={wsgi.CONTEXT_KEY: self.context})
|
||||||
|
|
||||||
|
name = self.random_name(
|
||||||
|
name='backup', prefix='TestBackupController')
|
||||||
|
body = {
|
||||||
|
'backup': {
|
||||||
|
"name": name,
|
||||||
|
"restore_from": {
|
||||||
|
"remote_location": "http://192.168.206.8:8080/v1/"
|
||||||
|
"AUTH_055b2fb9a2264ae5a5f6b3cc066c4a1d/"
|
||||||
|
"fake-container/fake-object",
|
||||||
|
"local_datastore_version_id": self.ds_version.id,
|
||||||
|
"size": 0.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret = self.controller.create(req, body, self.context.project_id)
|
||||||
|
self.assertEqual(202, ret.status)
|
||||||
|
|
||||||
|
ret_backup = ret.data(None)['backup']
|
||||||
|
|
||||||
|
self.assertEqual(BackupState.RESTORED, ret_backup.get('status'))
|
||||||
|
self.assertEqual(name, ret_backup.get('name'))
|
@ -1037,10 +1037,8 @@ class BackupTasksTest(trove_testtools.TestCase):
|
|||||||
|
|
||||||
self.assertTrue(self.backup.deleted)
|
self.assertTrue(self.backup.deleted)
|
||||||
|
|
||||||
@patch('trove.taskmanager.models.LOG')
|
|
||||||
@patch('trove.common.clients.create_swift_client')
|
@patch('trove.common.clients.create_swift_client')
|
||||||
def test_delete_backup_fail_delete_manifest(self, mock_swift_client,
|
def test_delete_backup_fail_delete_manifest(self, mock_swift_client):
|
||||||
mock_logging):
|
|
||||||
client_mock = MagicMock()
|
client_mock = MagicMock()
|
||||||
client_mock.head_object.return_value = {}
|
client_mock.head_object.return_value = {}
|
||||||
client_mock.delete_object.side_effect = ClientException("foo")
|
client_mock.delete_object.side_effect = ClientException("foo")
|
||||||
@ -1070,6 +1068,12 @@ class BackupTasksTest(trove_testtools.TestCase):
|
|||||||
client_mock.delete_object.assert_called_once_with('container',
|
client_mock.delete_object.assert_called_once_with('container',
|
||||||
'12e48.xbstream.gz')
|
'12e48.xbstream.gz')
|
||||||
|
|
||||||
|
def test_delete_backup_restored(self):
|
||||||
|
self.backup.state = state.BackupState.RESTORED
|
||||||
|
taskmanager_models.BackupTasks.delete_backup(mock.ANY, self.backup.id)
|
||||||
|
|
||||||
|
self.assertTrue(self.backup.deleted)
|
||||||
|
|
||||||
def test_parse_manifest(self):
|
def test_parse_manifest(self):
|
||||||
manifest = 'container/prefix'
|
manifest = 'container/prefix'
|
||||||
cont, prefix = taskmanager_models.BackupTasks._parse_manifest(manifest)
|
cont, prefix = taskmanager_models.BackupTasks._parse_manifest(manifest)
|
||||||
|
Loading…
Reference in New Issue
Block a user