Export and import backup service metadata

Add new admin API for backup-export and backup-import.
The new commands export the backup details (not actual backup) to
a string that can be imported again in another OpenStack cloud or
if the backup database was corrupted.
The code includes a default backup driver implementation.
Backup test code converted to use mock.

blueprint cinder-backup-recover-api
DocImpact new admin API calls backup-import and backup-export

Change-Id: I564194929962e75c67630e73d8711ee6587706d4
This commit is contained in:
Ronen Kat 2014-01-23 11:58:58 +02:00
parent 97c7c7f2c6
commit 1d6235cc3f
11 changed files with 1030 additions and 128 deletions

View File

@ -29,7 +29,6 @@ from cinder import exception
from cinder.openstack.common import log as logging
from cinder import utils
LOG = logging.getLogger(__name__)
@ -52,6 +51,11 @@ def make_backup_restore(elem):
elem.set('volume_id')
def make_backup_export_import_record(elem):
elem.set('backup_service')
elem.set('backup_url')
class BackupTemplate(xmlutil.TemplateBuilder):
def construct(self):
root = xmlutil.TemplateElement('backup', selector='backup')
@ -80,6 +84,16 @@ class BackupRestoreTemplate(xmlutil.TemplateBuilder):
return xmlutil.MasterTemplate(root, 1, nsmap={alias: namespace})
class BackupExportImportTemplate(xmlutil.TemplateBuilder):
def construct(self):
root = xmlutil.TemplateElement('backup-record',
selector='backup-record')
make_backup_export_import_record(root)
alias = Backups.alias
namespace = Backups.namespace
return xmlutil.MasterTemplate(root, 1, nsmap={alias: namespace})
class CreateDeserializer(wsgi.MetadataXMLDeserializer):
def default(self, string):
dom = utils.safe_minidom_parse_string(string)
@ -113,6 +127,25 @@ class RestoreDeserializer(wsgi.MetadataXMLDeserializer):
return restore
class BackupImportDeserializer(wsgi.MetadataXMLDeserializer):
def default(self, string):
dom = utils.safe_minidom_parse_string(string)
backup = self._extract_backup(dom)
retval = {'body': {'backup-record': backup}}
return retval
def _extract_backup(self, node):
backup = {}
backup_node = self.find_first_child_named(node, 'backup-record')
attributes = ['backup_service', 'backup_url']
for attr in attributes:
if backup_node.getAttribute(attr):
backup[attr] = backup_node.getAttribute(attr)
return backup
class BackupsController(wsgi.Controller):
"""The Backups API controller for the OpenStack API."""
@ -260,6 +293,61 @@ class BackupsController(wsgi.Controller):
req, dict(new_restore.iteritems()))
return retval
@wsgi.response(200)
@wsgi.serializers(xml=BackupExportImportTemplate)
def export_record(self, req, id):
"""Export a backup."""
LOG.debug(_('export record called for member %s.'), id)
context = req.environ['cinder.context']
try:
backup_info = self.backup_api.export_record(context, id)
except exception.BackupNotFound as error:
raise exc.HTTPNotFound(explanation=error.msg)
except exception.InvalidBackup as error:
raise exc.HTTPBadRequest(explanation=error.msg)
retval = self._view_builder.export_summary(
req, dict(backup_info.iteritems()))
LOG.debug(_('export record output: %s.'), retval)
return retval
@wsgi.response(201)
@wsgi.serializers(xml=BackupTemplate)
@wsgi.deserializers(xml=BackupImportDeserializer)
def import_record(self, req, body):
"""Import a backup."""
LOG.debug(_('Importing record from %s.'), body)
if not self.is_valid_body(body, 'backup-record'):
msg = _("Incorrect request body format.")
raise exc.HTTPBadRequest(explanation=msg)
context = req.environ['cinder.context']
import_data = body['backup-record']
#Verify that body elements are provided
try:
backup_service = import_data['backup_service']
backup_url = import_data['backup_url']
except KeyError:
msg = _("Incorrect request body format.")
raise exc.HTTPBadRequest(explanation=msg)
LOG.debug(_('Importing backup using %(service)s and url %(url)s.'),
{'service': backup_service, 'url': backup_url})
try:
new_backup = self.backup_api.import_record(context,
backup_service,
backup_url)
except exception.BackupNotFound as error:
raise exc.HTTPNotFound(explanation=error.msg)
except exception.InvalidBackup as error:
raise exc.HTTPBadRequest(explanation=error.msg)
except exception.ServiceNotFound as error:
raise exc.HTTPInternalServerError(explanation=error.msg)
retval = self._view_builder.summary(req, dict(new_backup.iteritems()))
LOG.debug(_('import record output: %s.'), retval)
return retval
class Backups(extensions.ExtensionDescriptor):
"""Backups support."""
@ -273,7 +361,7 @@ class Backups(extensions.ExtensionDescriptor):
resources = []
res = extensions.ResourceExtension(
Backups.alias, BackupsController(),
collection_actions={'detail': 'GET'},
member_actions={'restore': 'POST'})
collection_actions={'detail': 'GET', 'import_record': 'POST'},
member_actions={'restore': 'POST', 'export_record': 'GET'})
resources.append(res)
return resources

View File

@ -88,3 +88,12 @@ class ViewBuilder(common.ViewBuilder):
backups_dict['backups_links'] = backups_links
return backups_dict
def export_summary(self, request, export):
"""Generic view of an export."""
return {
'backup-record': {
'backup_service': export['backup_service'],
'backup_url': export['backup_url'],
},
}

View File

@ -96,6 +96,16 @@ class API(base.Base):
return True
return False
def _list_backup_services(self):
"""List all enabled backup services.
:returns: list -- hosts for services that are enabled for backup.
"""
topic = CONF.backup_topic
ctxt = context.get_admin_context()
services = self.db.service_get_all_by_topic(ctxt, topic)
return [srv['host'] for srv in services if not srv['disabled']]
def create(self, context, name, description, volume_id,
container, availability_zone=None):
"""Make the RPC call to create a volume backup."""
@ -197,3 +207,68 @@ class API(base.Base):
'volume_id': volume_id, }
return d
def export_record(self, context, backup_id):
"""Make the RPC call to export a volume backup.
Call backup manager to execute backup export.
:param context: running context
:param backup_id: backup id to export
:returns: dictionary -- a description of how to import the backup
:returns: contains 'backup_url' and 'backup_service'
:raises: InvalidBackup
"""
check_policy(context, 'backup-export')
backup = self.get(context, backup_id)
if backup['status'] != 'available':
msg = (_('Backup status must be available and not %s.') %
backup['status'])
raise exception.InvalidBackup(reason=msg)
LOG.debug("Calling RPCAPI with context: "
"%(ctx)s, host: %(host)s, backup: %(id)s.",
{'ctx': context,
'host': backup['host'],
'id': backup['id']})
export_data = self.backup_rpcapi.export_record(context,
backup['host'],
backup['id'])
return export_data
def import_record(self, context, backup_service, backup_url):
"""Make the RPC call to import a volume backup.
:param context: running context
:param backup_service: backup service name
:param backup_url: backup description to be used by the backup driver
:raises: InvalidBackup
:raises: ServiceNotFound
"""
check_policy(context, 'backup-import')
# NOTE(ronenkat): since we don't have a backup-scheduler
# we need to find a host that support the backup service
# that was used to create the backup.
# We send it to the first backup service host, and the backup manager
# on that host will forward it to other hosts on the hosts list if it
# cannot support correct service itself.
hosts = self._list_backup_services()
if len(hosts) == 0:
raise exception.ServiceNotFound(service_id=backup_service)
options = {'user_id': context.user_id,
'project_id': context.project_id,
'volume_id': '0000-0000-0000-0000',
'status': 'creating', }
backup = self.db.backup_create(context, options)
first_host = hosts.pop()
self.backup_rpcapi.import_record(context,
first_host,
backup['id'],
backup_service,
backup_url,
hosts)
return backup

View File

@ -263,3 +263,38 @@ class BackupDriver(base.Base):
def delete(self, backup):
"""Delete a saved backup."""
raise NotImplementedError()
def export_record(self, backup):
"""Export backup record.
Default backup driver implementation.
Serialize the backup record describing the backup into a string.
:param backup: backup entry to export
:returns backup_url - a string describing the backup record
"""
retval = jsonutils.dumps(backup)
return retval.encode("base64")
def import_record(self, backup_url):
"""Import and verify backup record.
Default backup driver implementation.
De-serialize the backup record into a dictionary, so we can
update the database.
:param backup_url: driver specific backup record string
:returns dictionary object with database updates
"""
return jsonutils.loads(backup_url.decode("base64"))
def verify(self, backup):
"""Verify that the backup exists on the backend.
Verify that the backup is OK, possibly following an import record
operation.
:param backup: backup id of the backup to verify
:raises: InvalidBackup, NotImplementedError
"""
raise NotImplementedError()

View File

@ -35,6 +35,7 @@ Volume backups can be created, restored, deleted and listed.
from oslo.config import cfg
from cinder.backup import rpcapi as backup_rpcapi
from cinder import context
from cinder import exception
from cinder import manager
@ -71,6 +72,7 @@ class BackupManager(manager.SchedulerDependentManager):
self.az = CONF.storage_availability_zone
self.volume_managers = {}
self._setup_volume_drivers()
self.backup_rpcapi = backup_rpcapi.BackupAPI()
super(BackupManager, self).__init__(service_name='backup',
*args, **kwargs)
@ -296,22 +298,20 @@ class BackupManager(manager.SchedulerDependentManager):
expected_status = 'restoring-backup'
actual_status = volume['status']
if actual_status != expected_status:
err = _('Restore backup aborted: expected volume status '
'%(expected_status)s but got %(actual_status)s.') % {
'expected_status': expected_status,
'actual_status': actual_status
}
err = (_('Restore backup aborted, expected volume status '
'%(expected_status)s but got %(actual_status)s.') %
{'expected_status': expected_status,
'actual_status': actual_status})
self.db.backup_update(context, backup_id, {'status': 'available'})
raise exception.InvalidVolume(reason=err)
expected_status = 'restoring'
actual_status = backup['status']
if actual_status != expected_status:
err = _('Restore backup aborted: expected backup status '
'%(expected_status)s but got %(actual_status)s.') % {
'expected_status': expected_status,
'actual_status': actual_status
}
err = (_('Restore backup aborted: expected backup status '
'%(expected_status)s but got %(actual_status)s.') %
{'expected_status': expected_status,
'actual_status': actual_status})
self.db.backup_update(context, backup_id, {'status': 'error',
'fail_reason': err})
self.db.volume_update(context, volume_id, {'status': 'error'})
@ -420,3 +420,151 @@ class BackupManager(manager.SchedulerDependentManager):
context = context.elevated()
self.db.backup_destroy(context, backup_id)
LOG.info(_('Delete backup finished, backup %s deleted.'), backup_id)
def export_record(self, context, backup_id):
"""Export all volume backup metadata details to allow clean import.
Export backup metadata so it could be re-imported into the database
without any prerequisite in the backup database.
:param context: running context
:param backup_id: backup id to export
:returns: backup_record - a description of how to import the backup
:returns: contains 'backup_url' - how to import the backup, and
:returns: 'backup_service' describing the needed driver.
:raises: InvalidBackup
"""
LOG.info(_('Export record started, backup: %s.'), backup_id)
backup = self.db.backup_get(context, backup_id)
expected_status = 'available'
actual_status = backup['status']
if actual_status != expected_status:
err = (_('Export backup aborted, expected backup status '
'%(expected_status)s but got %(actual_status)s.') %
{'expected_status': expected_status,
'actual_status': actual_status})
raise exception.InvalidBackup(reason=err)
backup_record = {}
backup_record['backup_service'] = backup['service']
backup_service = self._map_service_to_driver(backup['service'])
configured_service = self.driver_name
if backup_service != configured_service:
err = (_('Export record aborted, the backup service currently'
' configured [%(configured_service)s] is not the'
' backup service that was used to create this'
' backup [%(backup_service)s].') %
{'configured_service': configured_service,
'backup_service': backup_service})
raise exception.InvalidBackup(reason=err)
# Call driver to create backup description string
try:
utils.require_driver_initialized(self.driver)
backup_service = self.service.get_backup_driver(context)
backup_url = backup_service.export_record(backup)
backup_record['backup_url'] = backup_url
except Exception as err:
msg = unicode(err)
raise exception.InvalidBackup(reason=msg)
LOG.info(_('Export record finished, backup %s exported.'), backup_id)
return backup_record
def import_record(self,
context,
backup_id,
backup_service,
backup_url,
backup_hosts):
"""Import all volume backup metadata details to the backup db.
:param context: running context
:param backup_id: The new backup id for the import
:param backup_service: The needed backup driver for import
:param backup_url: An identifier string to locate the backup
:param backup_hosts: Potential hosts to execute the import
:raises: InvalidBackup
:raises: ServiceNotFound
"""
LOG.info(_('Import record started, backup_url: %s.'), backup_url)
# Can we import this backup?
if (backup_service != self.driver_name):
# No, are there additional potential backup hosts in the list?
if len(backup_hosts) > 0:
# try the next host on the list, maybe he can import
first_host = backup_hosts.pop()
self.backup_rpcapi.import_record(context,
first_host,
backup_id,
backup_service,
backup_url,
backup_hosts)
else:
# empty list - we are the last host on the list, fail
err = _('Import record failed, cannot find backup '
'service to perform the import. Request service '
'%(service)s') % {'service': backup_service}
self.db.backup_update(context, backup_id, {'status': 'error',
'fail_reason': err})
raise exception.ServiceNotFound(service_id=backup_service)
else:
# Yes...
try:
utils.require_driver_initialized(self.driver)
backup_service = self.service.get_backup_driver(context)
backup_options = backup_service.import_record(backup_url)
except Exception as err:
msg = unicode(err)
self.db.backup_update(context,
backup_id,
{'status': 'error',
'fail_reason': msg})
raise exception.InvalidBackup(reason=msg)
required_import_options = ['display_name',
'display_description',
'container',
'size',
'service_metadata',
'service',
'object_count']
backup_update = {}
backup_update['status'] = 'available'
backup_update['service'] = self.driver_name
backup_update['availability_zone'] = self.az
backup_update['host'] = self.host
for entry in required_import_options:
if entry not in backup_options:
msg = (_('Backup metadata received from driver for '
'import is missing %s.'), entry)
self.db.backup_update(context,
backup_id,
{'status': 'error',
'fail_reason': msg})
raise exception.InvalidBackup(reason=msg)
backup_update[entry] = backup_options[entry]
# Update the database
self.db.backup_update(context, backup_id, backup_update)
# Verify backup
try:
backup_service.verify(backup_id)
except NotImplementedError:
LOG.warn(_('Backup service %(service)s does not support '
'verify. Backup id %(id)s is not verified. '
'Skipping verify.') % {'service': self.driver_name,
'id': backup_id})
except exception.InvalidBackup as err:
with excutils.save_and_reraise_exception():
self.db.backup_update(context, backup_id,
{'status': 'error',
'fail_reason':
unicode(err)})
LOG.info(_('Import record id %s metadata from driver '
'finished.') % backup_id)

View File

@ -65,9 +65,42 @@ class BackupAPI(cinder.openstack.common.rpc.proxy.RpcProxy):
topic=topic)
def delete_backup(self, ctxt, host, backup_id):
LOG.debug("delete_backup rpcapi backup_id %s", backup_id)
LOG.debug("delete_backup rpcapi backup_id %s", backup_id)
topic = rpc.queue_get_for(ctxt, self.topic, host)
self.cast(ctxt,
self.make_msg('delete_backup',
backup_id=backup_id),
topic=topic)
def export_record(self, ctxt, host, backup_id):
LOG.debug("export_record in rpcapi backup_id %(id)s "
"on host %(host)s.",
{'id': backup_id,
'host': host})
topic = rpc.queue_get_for(ctxt, self.topic, host)
LOG.debug("export queue topic=%s" % topic)
return self.call(ctxt,
self.make_msg('export_record',
backup_id=backup_id),
topic=topic)
def import_record(self,
ctxt,
host,
backup_id,
backup_service,
backup_url,
backup_hosts):
LOG.debug("import_record rpcapi backup id $(id)s "
"on host %(host)s "
"for backup_url %(url)s." % {'id': backup_id,
'host': host,
'url': backup_url})
topic = rpc.queue_get_for(ctxt, self.topic, host)
self.cast(ctxt,
self.make_msg('import_record',
backup_id=backup_id,
backup_service=backup_service,
backup_url=backup_url,
backup_hosts=backup_hosts),
topic=topic)

View File

@ -18,6 +18,7 @@ Tests for Backup code.
"""
import json
import mock
from xml.dom import minidom
import webob
@ -81,11 +82,6 @@ class BackupsAPITestCase(test.TestCase):
return db.backup_get(context.get_admin_context(),
backup_id)[attrib_name]
@staticmethod
def _stub_service_get_all_by_topic(context, topic):
return [{'availability_zone': "fake_az", 'host': 'test_host',
'disabled': 0, 'updated_at': timeutils.utcnow()}]
def test_show_backup(self):
volume_id = utils.create_volume(self.context, size=5,
status='creating')['id']
@ -340,9 +336,11 @@ class BackupsAPITestCase(test.TestCase):
db.backup_destroy(context.get_admin_context(), backup_id2)
db.backup_destroy(context.get_admin_context(), backup_id1)
def test_create_backup_json(self):
self.stubs.Set(cinder.db, 'service_get_all_by_topic',
self._stub_service_get_all_by_topic)
@mock.patch('cinder.db.service_get_all_by_topic')
def test_create_backup_json(self, _mock_service_get_all_by_topic):
_mock_service_get_all_by_topic.return_value = [
{'availability_zone': "fake_az", 'host': 'test_host',
'disabled': 0, 'updated_at': timeutils.utcnow()}]
volume_id = utils.create_volume(self.context, size=5)['id']
@ -364,12 +362,16 @@ class BackupsAPITestCase(test.TestCase):
self.assertEqual(res.status_int, 202)
self.assertIn('id', res_dict['backup'])
self.assertTrue(_mock_service_get_all_by_topic.called)
db.volume_destroy(context.get_admin_context(), volume_id)
def test_create_backup_xml(self):
self.stubs.Set(cinder.db, 'service_get_all_by_topic',
self._stub_service_get_all_by_topic)
@mock.patch('cinder.db.service_get_all_by_topic')
def test_create_backup_xml(self, _mock_service_get_all_by_topic):
_mock_service_get_all_by_topic.return_value = [
{'availability_zone': "fake_az", 'host': 'test_host',
'disabled': 0, 'updated_at': timeutils.utcnow()}]
volume_id = utils.create_volume(self.context, size=2)['id']
req = webob.Request.blank('/v2/fake/backups')
@ -385,6 +387,7 @@ class BackupsAPITestCase(test.TestCase):
dom = minidom.parseString(res.body)
backup = dom.getElementsByTagName('backup')
self.assertTrue(backup.item(0).hasAttribute('id'))
self.assertTrue(_mock_service_get_all_by_topic.called)
db.volume_destroy(context.get_admin_context(), volume_id)
@ -468,13 +471,13 @@ class BackupsAPITestCase(test.TestCase):
'Invalid volume: Volume to be backed up must'
' be available')
def test_create_backup_WithOUT_enabled_backup_service(self):
@mock.patch('cinder.db.service_get_all_by_topic')
def test_create_backup_WithOUT_enabled_backup_service(
self,
_mock_service_get_all_by_topic):
# need an enabled backup service available
def stub_empty_service_get_all_by_topic(ctxt, topic):
return []
_mock_service_get_all_by_topic.return_value = []
self.stubs.Set(cinder.db, 'service_get_all_by_topic',
stub_empty_service_get_all_by_topic)
volume_id = utils.create_volume(self.context, size=2)['id']
req = webob.Request.blank('/v2/fake/backups')
body = {"backup": {"display_name": "nightly001",
@ -499,76 +502,72 @@ class BackupsAPITestCase(test.TestCase):
volume = self.volume_api.get(context.get_admin_context(), volume_id)
self.assertEqual(volume['status'], 'available')
def test_is_backup_service_enabled(self):
def empty_service(ctxt, topic):
return []
@mock.patch('cinder.db.service_get_all_by_topic')
def test_is_backup_service_enabled(self, _mock_service_get_all_by_topic):
test_host = 'test_host'
alt_host = 'strange_host'
empty_service = []
#service host not match with volume's host
def host_not_match(context, topic):
return [{'availability_zone': "fake_az", 'host': alt_host,
'disabled': 0, 'updated_at': timeutils.utcnow()}]
host_not_match = [{'availability_zone': "fake_az", 'host': alt_host,
'disabled': 0, 'updated_at': timeutils.utcnow()}]
#service az not match with volume's az
def az_not_match(context, topic):
return [{'availability_zone': "strange_az", 'host': test_host,
'disabled': 0, 'updated_at': timeutils.utcnow()}]
az_not_match = [{'availability_zone': "strange_az", 'host': test_host,
'disabled': 0, 'updated_at': timeutils.utcnow()}]
#service disabled
def disabled_service(context, topic):
return [{'availability_zone': "fake_az", 'host': test_host,
'disabled': 1, 'updated_at': timeutils.utcnow()}]
disabled_service = [{'availability_zone': "fake_az",
'host': test_host,
'disabled': 1,
'updated_at': timeutils.utcnow()}]
#dead service that last reported at 20th centry
def dead_service(context, topic):
return [{'availability_zone': "fake_az", 'host': alt_host,
'disabled': 0, 'updated_at': '1989-04-16 02:55:44'}]
dead_service = [{'availability_zone': "fake_az", 'host': alt_host,
'disabled': 0, 'updated_at': '1989-04-16 02:55:44'}]
#first service's host not match but second one works.
def multi_services(context, topic):
return [{'availability_zone': "fake_az", 'host': alt_host,
'disabled': 0, 'updated_at': timeutils.utcnow()},
{'availability_zone': "fake_az", 'host': test_host,
'disabled': 0, 'updated_at': timeutils.utcnow()}]
multi_services = [{'availability_zone': "fake_az", 'host': alt_host,
'disabled': 0, 'updated_at': timeutils.utcnow()},
{'availability_zone': "fake_az", 'host': test_host,
'disabled': 0, 'updated_at': timeutils.utcnow()}]
#Setup mock to run through the following service cases
_mock_service_get_all_by_topic.side_effect = [empty_service,
host_not_match,
az_not_match,
disabled_service,
dead_service,
multi_services]
volume_id = utils.create_volume(self.context, size=2,
host=test_host)['id']
volume = self.volume_api.get(context.get_admin_context(), volume_id)
#test empty service
self.stubs.Set(cinder.db, 'service_get_all_by_topic', empty_service)
self.assertEqual(self.backup_api._is_backup_service_enabled(volume,
test_host),
False)
#test host not match service
self.stubs.Set(cinder.db, 'service_get_all_by_topic', host_not_match)
self.assertEqual(self.backup_api._is_backup_service_enabled(volume,
test_host),
False)
#test az not match service
self.stubs.Set(cinder.db, 'service_get_all_by_topic', az_not_match)
self.assertEqual(self.backup_api._is_backup_service_enabled(volume,
test_host),
False)
#test disabled service
self.stubs.Set(cinder.db, 'service_get_all_by_topic', disabled_service)
self.assertEqual(self.backup_api._is_backup_service_enabled(volume,
test_host),
False)
#test dead service
self.stubs.Set(cinder.db, 'service_get_all_by_topic', dead_service)
self.assertEqual(self.backup_api._is_backup_service_enabled(volume,
test_host),
False)
#test multi services and the last service matches
self.stubs.Set(cinder.db, 'service_get_all_by_topic', multi_services)
self.assertEqual(self.backup_api._is_backup_service_enabled(volume,
test_host),
True)
@ -708,16 +707,17 @@ class BackupsAPITestCase(test.TestCase):
self.assertEqual(res_dict['badRequest']['message'],
'Incorrect request body format')
def test_restore_backup_volume_id_unspecified(self):
@mock.patch('cinder.volume.API.create')
def test_restore_backup_volume_id_unspecified(self,
_mock_volume_api_create):
# intercept volume creation to ensure created volume
# has status of available
def fake_volume_api_create(cls, context, size, name, description):
def fake_volume_api_create(context, size, name, description):
volume_id = utils.create_volume(self.context, size=size)['id']
return db.volume_get(context, volume_id)
self.stubs.Set(cinder.volume.API, 'create',
fake_volume_api_create)
_mock_volume_api_create.side_effect = fake_volume_api_create
backup_id = self._create_backup(size=5, status='available')
@ -733,16 +733,13 @@ class BackupsAPITestCase(test.TestCase):
self.assertEqual(res.status_int, 202)
self.assertEqual(res_dict['restore']['backup_id'], backup_id)
def test_restore_backup_with_InvalidInput(self):
@mock.patch('cinder.backup.API.restore')
def test_restore_backup_with_InvalidInput(self,
_mock_volume_api_restore):
def fake_backup_api_restore_throwing_InvalidInput(cls, context,
backup_id,
volume_id):
msg = _("Invalid input")
raise exception.InvalidInput(reason=msg)
self.stubs.Set(cinder.backup.API, 'restore',
fake_backup_api_restore_throwing_InvalidInput)
msg = _("Invalid input")
_mock_volume_api_restore.side_effect = \
exception.InvalidInput(reason=msg)
backup_id = self._create_backup(status='available')
# need to create the volume referenced below first
@ -846,18 +843,15 @@ class BackupsAPITestCase(test.TestCase):
db.backup_destroy(context.get_admin_context(), backup_id)
def test_restore_backup_with_VolumeSizeExceedsAvailableQuota(self):
@mock.patch('cinder.backup.API.restore')
def test_restore_backup_with_VolumeSizeExceedsAvailableQuota(
self,
_mock_backup_restore):
def fake_backup_api_restore_throwing_VolumeSizeExceedsAvailableQuota(
cls, context, backup_id, volume_id):
raise exception.VolumeSizeExceedsAvailableQuota(requested='2',
consumed='2',
quota='3')
self.stubs.Set(
cinder.backup.API,
'restore',
fake_backup_api_restore_throwing_VolumeSizeExceedsAvailableQuota)
_mock_backup_restore.side_effect = \
exception.VolumeSizeExceedsAvailableQuota(requested='2',
consumed='2',
quota='3')
backup_id = self._create_backup(status='available')
# need to create the volume referenced below first
@ -880,16 +874,12 @@ class BackupsAPITestCase(test.TestCase):
'Gigabytes quota. Requested 2G, quota is 3G and '
'2G has been consumed.')
def test_restore_backup_with_VolumeLimitExceeded(self):
@mock.patch('cinder.backup.API.restore')
def test_restore_backup_with_VolumeLimitExceeded(self,
_mock_backup_restore):
def fake_backup_api_restore_throwing_VolumeLimitExceeded(cls,
context,
backup_id,
volume_id):
raise exception.VolumeLimitExceeded(allowed=1)
self.stubs.Set(cinder.backup.API, 'restore',
fake_backup_api_restore_throwing_VolumeLimitExceeded)
_mock_backup_restore.side_effect = \
exception.VolumeLimitExceeded(allowed=1)
backup_id = self._create_backup(status='available')
# need to create the volume referenced below first
@ -956,3 +946,315 @@ class BackupsAPITestCase(test.TestCase):
db.volume_destroy(context.get_admin_context(), volume_id)
db.backup_destroy(context.get_admin_context(), backup_id)
def test_export_record_as_non_admin(self):
backup_id = self._create_backup(status='available', size=10)
req = webob.Request.blank('/v2/fake/backups/%s/export_record' %
backup_id)
req.method = 'GET'
req.headers['content-type'] = 'application/json'
res = req.get_response(fakes.wsgi_app())
# request is not authorized
self.assertEqual(res.status_int, 403)
@mock.patch('cinder.backup.rpcapi.BackupAPI.export_record')
def test_export_backup_record_id_specified_json(self,
_mock_export_record_rpc):
backup_id = self._create_backup(status='available', size=10)
ctx = context.RequestContext('admin', 'fake', is_admin=True)
backup_service = 'fake'
backup_url = 'fake'
_mock_export_record_rpc.return_value = \
{'backup_service': backup_service,
'backup_url': backup_url}
req = webob.Request.blank('/v2/fake/backups/%s/export_record' %
backup_id)
req.method = 'GET'
req.headers['content-type'] = 'application/json'
res = req.get_response(fakes.wsgi_app(fake_auth_context=ctx))
res_dict = json.loads(res.body)
# verify that request is successful
self.assertEqual(res.status_int, 200)
self.assertEqual(res_dict['backup-record']['backup_service'],
backup_service)
self.assertEqual(res_dict['backup-record']['backup_url'],
backup_url)
db.backup_destroy(context.get_admin_context(), backup_id)
@mock.patch('cinder.backup.rpcapi.BackupAPI.export_record')
def test_export_record_backup_id_specified_xml(self,
_mock_export_record_rpc):
backup_id = self._create_backup(status='available', size=10)
ctx = context.RequestContext('admin', 'fake', is_admin=True)
backup_service = 'fake'
backup_url = 'fake'
_mock_export_record_rpc.return_value = \
{'backup_service': backup_service,
'backup_url': backup_url}
req = webob.Request.blank('/v2/fake/backups/%s/export_record' %
backup_id)
req.method = 'GET'
req.headers['Content-Type'] = 'application/xml'
req.headers['Accept'] = 'application/xml'
res = req.get_response(fakes.wsgi_app(fake_auth_context=ctx))
self.assertEqual(res.status_int, 200)
dom = minidom.parseString(res.body)
export = dom.getElementsByTagName('backup-record')
self.assertEqual(export.item(0).getAttribute('backup_service'),
backup_service)
self.assertEqual(export.item(0).getAttribute('backup_url'),
backup_url)
#db.backup_destroy(context.get_admin_context(), backup_id)
def test_export_record_with_bad_backup_id(self):
ctx = context.RequestContext('admin', 'fake', is_admin=True)
backup_id = 'bad_id'
req = webob.Request.blank('/v2/fake/backups/%s/export_record' %
backup_id)
req.method = 'GET'
req.headers['content-type'] = 'application/json'
res = req.get_response(fakes.wsgi_app(fake_auth_context=ctx))
res_dict = json.loads(res.body)
self.assertEqual(res.status_int, 404)
self.assertEqual(res_dict['itemNotFound']['code'], 404)
self.assertEqual(res_dict['itemNotFound']['message'],
'Backup %s could not be found.' % backup_id)
def test_export_record_for_unavailable_backup(self):
backup_id = self._create_backup(status='restoring')
ctx = context.RequestContext('admin', 'fake', is_admin=True)
req = webob.Request.blank('/v2/fake/backups/%s/export_record' %
backup_id)
req.method = 'GET'
req.headers['content-type'] = 'application/json'
res = req.get_response(fakes.wsgi_app(fake_auth_context=ctx))
res_dict = json.loads(res.body)
self.assertEqual(res.status_int, 400)
self.assertEqual(res_dict['badRequest']['code'], 400)
self.assertEqual(res_dict['badRequest']['message'],
'Invalid backup: Backup status must be available '
'and not restoring.')
db.backup_destroy(context.get_admin_context(), backup_id)
@mock.patch('cinder.backup.rpcapi.BackupAPI.export_record')
def test_export_record_with_unavailable_service(self,
_mock_export_record_rpc):
msg = 'fake unavailable service'
_mock_export_record_rpc.side_effect = \
exception.InvalidBackup(reason=msg)
backup_id = self._create_backup(status='available')
ctx = context.RequestContext('admin', 'fake', is_admin=True)
req = webob.Request.blank('/v2/fake/backups/%s/export_record' %
backup_id)
req.method = 'GET'
req.headers['content-type'] = 'application/json'
res = req.get_response(fakes.wsgi_app(fake_auth_context=ctx))
res_dict = json.loads(res.body)
self.assertEqual(res.status_int, 400)
self.assertEqual(res_dict['badRequest']['code'], 400)
self.assertEqual(res_dict['badRequest']['message'],
'Invalid backup: %s' % msg)
db.backup_destroy(context.get_admin_context(), backup_id)
def test_import_record_as_non_admin(self):
backup_service = 'fake'
backup_url = 'fake'
req = webob.Request.blank('/v2/fake/backups/import_record')
body = {'backup-record': {'backup_service': backup_service,
'backup_url': backup_url}}
req.body = json.dumps(body)
req.method = 'POST'
req.headers['content-type'] = 'application/json'
res = req.get_response(fakes.wsgi_app())
# request is not authorized
self.assertEqual(res.status_int, 403)
@mock.patch('cinder.backup.api.API._list_backup_services')
@mock.patch('cinder.backup.rpcapi.BackupAPI.import_record')
def test_import_record_volume_id_specified_json(self,
_mock_import_record_rpc,
_mock_list_services):
ctx = context.RequestContext('admin', 'fake', is_admin=True)
backup_service = 'fake'
backup_url = 'fake'
_mock_import_record_rpc.return_value = \
{'display_name': 'fake',
'display_description': 'fake',
'container': 'fake',
'size': 1,
'service_metadata': 'fake',
'service': 'fake',
'object_count': 1,
'status': 'available',
'availability_zone': 'fake'}
_mock_list_services.return_value = ['fake']
req = webob.Request.blank('/v2/fake/backups/import_record')
body = {'backup-record': {'backup_service': backup_service,
'backup_url': backup_url}}
req.body = json.dumps(body)
req.method = 'POST'
req.headers['content-type'] = 'application/json'
res = req.get_response(fakes.wsgi_app(fake_auth_context=ctx))
res_dict = json.loads(res.body)
# verify that request is successful
self.assertEqual(res.status_int, 201)
self.assertTrue('id' in res_dict['backup'])
@mock.patch('cinder.backup.api.API._list_backup_services')
@mock.patch('cinder.backup.rpcapi.BackupAPI.import_record')
def test_import_record_volume_id_specified_xml(self,
_mock_import_record_rpc,
_mock_list_services):
ctx = context.RequestContext('admin', 'fake', is_admin=True)
backup_service = 'fake'
backup_url = 'fake'
_mock_import_record_rpc.return_value = \
{'display_name': 'fake',
'display_description': 'fake',
'container': 'fake',
'size': 1,
'service_metadata': 'fake',
'service': 'fake',
'object_count': 1,
'status': 'available',
'availability_zone': 'fake'}
_mock_list_services.return_value = ['fake']
req = webob.Request.blank('/v2/fake/backups/import_record')
req.body = ('<backup-record backup_service="%(backup_service)s" '
'backup_url="%(backup_url)s"/>') \
% {'backup_url': backup_url,
'backup_service': backup_service}
req.method = 'POST'
req.headers['Content-Type'] = 'application/xml'
req.headers['Accept'] = 'application/xml'
res = req.get_response(fakes.wsgi_app(fake_auth_context=ctx))
self.assertEqual(res.status_int, 201)
dom = minidom.parseString(res.body)
backup = dom.getElementsByTagName('backup')
self.assertTrue(backup.item(0).hasAttribute('id'))
@mock.patch('cinder.backup.api.API._list_backup_services')
def test_import_record_with_no_backup_services(self,
_mock_list_services):
ctx = context.RequestContext('admin', 'fake', is_admin=True)
backup_service = 'fake'
backup_url = 'fake'
_mock_list_services.return_value = []
req = webob.Request.blank('/v2/fake/backups/import_record')
body = {'backup-record': {'backup_service': backup_service,
'backup_url': backup_url}}
req.body = json.dumps(body)
req.method = 'POST'
req.headers['content-type'] = 'application/json'
res = req.get_response(fakes.wsgi_app(fake_auth_context=ctx))
res_dict = json.loads(res.body)
self.assertEqual(res.status_int, 500)
self.assertEqual(res_dict['computeFault']['code'], 500)
self.assertEqual(res_dict['computeFault']['message'],
'Service %s could not be found.'
% backup_service)
@mock.patch('cinder.backup.api.API._list_backup_services')
@mock.patch('cinder.backup.rpcapi.BackupAPI.import_record')
def test_import_backup_with_missing_backup_services(self,
_mock_import_record,
_mock_list_services):
ctx = context.RequestContext('admin', 'fake', is_admin=True)
backup_service = 'fake'
backup_url = 'fake'
_mock_list_services.return_value = ['no-match1', 'no-match2']
_mock_import_record.side_effect = \
exception.ServiceNotFound(service_id='fake')
req = webob.Request.blank('/v2/fake/backups/import_record')
body = {'backup-record': {'backup_service': backup_service,
'backup_url': backup_url}}
req.body = json.dumps(body)
req.method = 'POST'
req.headers['content-type'] = 'application/json'
res = req.get_response(fakes.wsgi_app(fake_auth_context=ctx))
res_dict = json.loads(res.body)
self.assertEqual(res.status_int, 500)
self.assertEqual(res_dict['computeFault']['code'], 500)
self.assertEqual(res_dict['computeFault']['message'],
'Service %s could not be found.'
% backup_service)
def test_import_record_with_missing_body_elements(self):
ctx = context.RequestContext('admin', 'fake', is_admin=True)
backup_service = 'fake'
backup_url = 'fake'
#test with no backup_service
req = webob.Request.blank('/v2/fake/backups/import_record')
body = {'backup-record': {'backup_url': backup_url}}
req.body = json.dumps(body)
req.method = 'POST'
req.headers['content-type'] = 'application/json'
res = req.get_response(fakes.wsgi_app(fake_auth_context=ctx))
res_dict = json.loads(res.body)
self.assertEqual(res.status_int, 400)
self.assertEqual(res_dict['badRequest']['code'], 400)
self.assertEqual(res_dict['badRequest']['message'],
'Incorrect request body format.')
#test with no backup_url
req = webob.Request.blank('/v2/fake/backups/import_record')
body = {'backup-record': {'backup_service': backup_service}}
req.body = json.dumps(body)
req.method = 'POST'
req.headers['content-type'] = 'application/json'
res = req.get_response(fakes.wsgi_app(fake_auth_context=ctx))
res_dict = json.loads(res.body)
self.assertEqual(res.status_int, 400)
self.assertEqual(res_dict['badRequest']['code'], 400)
self.assertEqual(res_dict['badRequest']['message'],
'Incorrect request body format.')
#test with no backup_url and backup_url
req = webob.Request.blank('/v2/fake/backups/import_record')
body = {'backup-record': {}}
req.body = json.dumps(body)
req.method = 'POST'
req.headers['content-type'] = 'application/json'
res = req.get_response(fakes.wsgi_app(fake_auth_context=ctx))
res_dict = json.loads(res.body)
self.assertEqual(res.status_int, 400)
self.assertEqual(res_dict['badRequest']['code'], 400)
self.assertEqual(res_dict['badRequest']['message'],
'Incorrect request body format.')
def test_import_record_with_no_body(self):
ctx = context.RequestContext('admin', 'fake', is_admin=True)
req = webob.Request.blank('/v2/fake/backups/import_record')
req.body = json.dumps(None)
req.method = 'POST'
req.headers['content-type'] = 'application/json'
res = req.get_response(fakes.wsgi_app(fake_auth_context=ctx))
res_dict = json.loads(res.body)
# verify that request is successful
self.assertEqual(res.status_int, 400)
self.assertEqual(res_dict['badRequest']['code'], 400)
self.assertEqual(res_dict['badRequest']['message'],
'Incorrect request body format.')

View File

@ -71,5 +71,8 @@
"backup:delete": [],
"backup:get": [],
"backup:get_all": [],
"backup:restore": []
"backup:restore": [],
"backup:backup-import": [["rule:admin_api"]],
"backup:backup-export": [["rule:admin_api"]]
}

View File

@ -17,6 +17,7 @@ Tests for Backup code.
"""
import mock
import tempfile
from oslo.config import cfg
@ -59,7 +60,7 @@ class BackupTestCase(test.TestCase):
display_description='this is a test backup',
container='volumebackups',
status='creating',
size=0,
size=1,
object_count=0,
project_id='fake'):
"""Create a backup entry in the DB.
@ -101,6 +102,31 @@ class BackupTestCase(test.TestCase):
vol['attach_status'] = 'detached'
return db.volume_create(self.ctxt, vol)['id']
def _create_exported_record_entry(self, vol_size=1):
"""Create backup metadata export entry."""
vol_id = self._create_volume_db_entry(status='available',
size=vol_size)
backup_id = self._create_backup_db_entry(status='available',
volume_id=vol_id)
export = self.backup_mgr.export_record(self.ctxt, backup_id)
return export
def _create_export_record_db_entry(self,
volume_id='0000',
status='creating',
project_id='fake'):
"""Create a backup entry in the DB.
Return the entry ID
"""
backup = {}
backup['volume_id'] = volume_id
backup['user_id'] = 'fake'
backup['project_id'] = project_id
backup['status'] = status
return db.backup_create(self.ctxt, backup)['id']
def test_init_host(self):
"""Make sure stuck volumes and backups are reset to correct
states when backup_manager.init_host() is called
@ -149,17 +175,13 @@ class BackupTestCase(test.TestCase):
self.ctxt,
backup_id)
def test_create_backup_with_error(self):
@mock.patch('%s.%s' % (CONF.volume_driver, 'backup_volume'))
def test_create_backup_with_error(self, _mock_volume_backup):
"""Test error handling when error occurs during backup creation."""
vol_id = self._create_volume_db_entry(size=1)
backup_id = self._create_backup_db_entry(volume_id=vol_id)
def fake_backup_volume(context, backup, backup_service):
raise FakeBackupException('fake')
self.stubs.Set(self.backup_mgr.driver, 'backup_volume',
fake_backup_volume)
_mock_volume_backup.side_effect = FakeBackupException('fake')
self.assertRaises(FakeBackupException,
self.backup_mgr.create_backup,
self.ctxt,
@ -168,25 +190,22 @@ class BackupTestCase(test.TestCase):
self.assertEqual(vol['status'], 'available')
backup = db.backup_get(self.ctxt, backup_id)
self.assertEqual(backup['status'], 'error')
self.assertTrue(_mock_volume_backup.called)
def test_create_backup(self):
@mock.patch('%s.%s' % (CONF.volume_driver, 'backup_volume'))
def test_create_backup(self, _mock_volume_backup):
"""Test normal backup creation."""
vol_size = 1
vol_id = self._create_volume_db_entry(size=vol_size)
backup_id = self._create_backup_db_entry(volume_id=vol_id)
def fake_backup_volume(context, backup, backup_service):
pass
self.stubs.Set(self.backup_mgr.driver, 'backup_volume',
fake_backup_volume)
self.backup_mgr.create_backup(self.ctxt, backup_id)
vol = db.volume_get(self.ctxt, vol_id)
self.assertEqual(vol['status'], 'available')
backup = db.backup_get(self.ctxt, backup_id)
self.assertEqual(backup['status'], 'available')
self.assertEqual(backup['size'], vol_size)
self.assertTrue(_mock_volume_backup.called)
def test_restore_backup_with_bad_volume_status(self):
"""Test error handling when restoring a backup to a volume
@ -220,19 +239,15 @@ class BackupTestCase(test.TestCase):
backup = db.backup_get(self.ctxt, backup_id)
self.assertEqual(backup['status'], 'error')
def test_restore_backup_with_driver_error(self):
@mock.patch('%s.%s' % (CONF.volume_driver, 'restore_backup'))
def test_restore_backup_with_driver_error(self, _mock_volume_restore):
"""Test error handling when an error occurs during backup restore."""
vol_id = self._create_volume_db_entry(status='restoring-backup',
size=1)
backup_id = self._create_backup_db_entry(status='restoring',
volume_id=vol_id)
def fake_restore_backup(context, backup, volume, backup_service):
raise FakeBackupException('fake')
self.stubs.Set(self.backup_mgr.driver, 'restore_backup',
fake_restore_backup)
_mock_volume_restore.side_effect = FakeBackupException('fake')
self.assertRaises(FakeBackupException,
self.backup_mgr.restore_backup,
self.ctxt,
@ -242,6 +257,7 @@ class BackupTestCase(test.TestCase):
self.assertEqual(vol['status'], 'error_restoring')
backup = db.backup_get(self.ctxt, backup_id)
self.assertEqual(backup['status'], 'available')
self.assertTrue(_mock_volume_restore.called)
def test_restore_backup_with_bad_service(self):
"""Test error handling when attempting a restore of a backup
@ -252,12 +268,6 @@ class BackupTestCase(test.TestCase):
backup_id = self._create_backup_db_entry(status='restoring',
volume_id=vol_id)
def fake_restore_backup(context, backup, volume, backup_service):
pass
self.stubs.Set(self.backup_mgr.driver, 'restore_backup',
fake_restore_backup)
service = 'cinder.tests.backup.bad_service'
db.backup_update(self.ctxt, backup_id, {'service': service})
self.assertRaises(exception.InvalidBackup,
@ -270,7 +280,8 @@ class BackupTestCase(test.TestCase):
backup = db.backup_get(self.ctxt, backup_id)
self.assertEqual(backup['status'], 'available')
def test_restore_backup(self):
@mock.patch('%s.%s' % (CONF.volume_driver, 'restore_backup'))
def test_restore_backup(self, _mock_volume_restore):
"""Test normal backup restoration."""
vol_size = 1
vol_id = self._create_volume_db_entry(status='restoring-backup',
@ -278,17 +289,12 @@ class BackupTestCase(test.TestCase):
backup_id = self._create_backup_db_entry(status='restoring',
volume_id=vol_id)
def fake_restore_backup(context, backup, volume, backup_service):
pass
self.stubs.Set(self.backup_mgr.driver, 'restore_backup',
fake_restore_backup)
self.backup_mgr.restore_backup(self.ctxt, backup_id, vol_id)
vol = db.volume_get(self.ctxt, vol_id)
self.assertEqual(vol['status'], 'available')
backup = db.backup_get(self.ctxt, backup_id)
self.assertEqual(backup['status'], 'available')
self.assertTrue(_mock_volume_restore.called)
def test_delete_backup_with_bad_backup_status(self):
"""Test error handling when deleting a backup with a backup
@ -418,3 +424,176 @@ class BackupTestCase(test.TestCase):
self.assertEqual('cinder.backup.drivers.swift',
backup_mgr.driver_name)
setattr(cfg.CONF, 'backup_driver', old_setting)
def test_export_record_with_bad_service(self):
"""Test error handling when attempting an export of a backup
record with a different service to that used to create the backup.
"""
vol_id = self._create_volume_db_entry(size=1)
backup_id = self._create_backup_db_entry(status='available',
volume_id=vol_id)
service = 'cinder.tests.backup.bad_service'
db.backup_update(self.ctxt, backup_id, {'service': service})
self.assertRaises(exception.InvalidBackup,
self.backup_mgr.export_record,
self.ctxt,
backup_id)
def test_export_record_with_bad_backup_status(self):
"""Test error handling when exporting a backup record with a backup
with a bad status.
"""
vol_id = self._create_volume_db_entry(status='available',
size=1)
backup_id = self._create_backup_db_entry(status='error',
volume_id=vol_id)
self.assertRaises(exception.InvalidBackup,
self.backup_mgr.export_record,
self.ctxt,
backup_id)
def test_export_record(self):
"""Test normal backup record export."""
vol_size = 1
vol_id = self._create_volume_db_entry(status='available',
size=vol_size)
backup_id = self._create_backup_db_entry(status='available',
volume_id=vol_id)
export = self.backup_mgr.export_record(self.ctxt, backup_id)
self.assertEqual(export['backup_service'], CONF.backup_driver)
self.assertTrue('backup_url' in export)
def test_import_record_with_verify_not_implemented(self):
"""Test normal backup record import.
Test the case when import succeeds for the case that the
driver does not support verify.
"""
vol_size = 1
export = self._create_exported_record_entry(vol_size=vol_size)
imported_record = self._create_export_record_db_entry()
backup_hosts = []
backup_driver = self.backup_mgr.service.get_backup_driver(self.ctxt)
_mock_backup_verify_class = ('%s.%s.%s' %
(backup_driver.__module__,
backup_driver.__class__.__name__,
'verify'))
with mock.patch(_mock_backup_verify_class) as _mock_record_verify:
_mock_record_verify.side_effect = NotImplementedError()
self.backup_mgr.import_record(self.ctxt,
imported_record,
export['backup_service'],
export['backup_url'],
backup_hosts)
backup = db.backup_get(self.ctxt, imported_record)
self.assertEqual(backup['status'], 'available')
self.assertEqual(backup['size'], vol_size)
def test_import_record_with_verify(self):
"""Test normal backup record import.
Test the case when import succeeds for the case that the
driver implements verify.
"""
vol_size = 1
export = self._create_exported_record_entry(vol_size=vol_size)
imported_record = self._create_export_record_db_entry()
backup_hosts = []
backup_driver = self.backup_mgr.service.get_backup_driver(self.ctxt)
_mock_backup_verify_class = ('%s.%s.%s' %
(backup_driver.__module__,
backup_driver.__class__.__name__,
'verify'))
with mock.patch(_mock_backup_verify_class) as _mock_record_verify:
self.backup_mgr.import_record(self.ctxt,
imported_record,
export['backup_service'],
export['backup_url'],
backup_hosts)
backup = db.backup_get(self.ctxt, imported_record)
self.assertEqual(backup['status'], 'available')
self.assertEqual(backup['size'], vol_size)
def test_import_record_with_bad_service(self):
"""Test error handling when attempting an import of a backup
record with a different service to that used to create the backup.
"""
export = self._create_exported_record_entry()
export['backup_service'] = 'cinder.tests.backup.bad_service'
imported_record = self._create_export_record_db_entry()
#Test the case where the additional hosts list is empty
backup_hosts = []
self.assertRaises(exception.ServiceNotFound,
self.backup_mgr.import_record,
self.ctxt,
imported_record,
export['backup_service'],
export['backup_url'],
backup_hosts)
#Test that the import backup keeps calling other hosts to find a
#suitable host for the backup service
backup_hosts = ['fake1', 'fake2']
BackupAPI_import = 'cinder.backup.rpcapi.BackupAPI.import_record'
with mock.patch(BackupAPI_import) as _mock_backup_import:
self.backup_mgr.import_record(self.ctxt,
imported_record,
export['backup_service'],
export['backup_url'],
backup_hosts)
self.assertTrue(_mock_backup_import.called)
def test_import_record_with_invalid_backup(self):
"""Test error handling when attempting an import of a backup
record where the backup driver returns an exception.
"""
export = self._create_exported_record_entry()
backup_driver = self.backup_mgr.service.get_backup_driver(self.ctxt)
_mock_record_import_class = ('%s.%s.%s' %
(backup_driver.__module__,
backup_driver.__class__.__name__,
'import_record'))
imported_record = self._create_export_record_db_entry()
backup_hosts = []
with mock.patch(_mock_record_import_class) as _mock_record_import:
_mock_record_import.side_effect = FakeBackupException('fake')
self.assertRaises(exception.InvalidBackup,
self.backup_mgr.import_record,
self.ctxt,
imported_record,
export['backup_service'],
export['backup_url'],
backup_hosts)
self.assertTrue(_mock_record_import.called)
backup = db.backup_get(self.ctxt, imported_record)
self.assertEqual(backup['status'], 'error')
def test_import_record_with_verify_invalid_backup(self):
"""Test error handling when attempting an import of a backup
record where the backup driver returns an exception.
"""
vol_size = 1
export = self._create_exported_record_entry(vol_size=vol_size)
imported_record = self._create_export_record_db_entry()
backup_hosts = []
backup_driver = self.backup_mgr.service.get_backup_driver(self.ctxt)
_mock_backup_verify_class = ('%s.%s.%s' %
(backup_driver.__module__,
backup_driver.__class__.__name__,
'verify'))
with mock.patch(_mock_backup_verify_class) as _mock_record_verify:
_mock_record_verify.side_effect = \
exception.InvalidBackup(reason='fake')
self.assertRaises(exception.InvalidBackup,
self.backup_mgr.import_record,
self.ctxt,
imported_record,
export['backup_service'],
export['backup_url'],
backup_hosts)
self.assertTrue(_mock_record_verify.called)
backup = db.backup_get(self.ctxt, imported_record)
self.assertEqual(backup['status'], 'error')

View File

@ -25,6 +25,14 @@ from cinder.openstack.common import jsonutils
from cinder import test
_backup_db_fields = ['id', 'user_id', 'project_id',
'volume_id', 'host', 'availability_zone',
'display_name', 'display_description',
'container', 'status', 'fail_reason',
'service_metadata', 'service', 'size',
'object_count']
class BackupBaseDriverTestCase(test.TestCase):
def _create_volume_db_entry(self, id, size):
@ -73,6 +81,26 @@ class BackupBaseDriverTestCase(test.TestCase):
json_metadata = self.driver.get_metadata(self.volume_id)
self.driver.put_metadata(self.volume_id, json_metadata)
def test_export_record(self):
export_string = self.driver.export_record(self.backup)
export_dict = jsonutils.loads(export_string.decode("base64"))
# Make sure we don't lose data when converting to string
for key in _backup_db_fields:
self.assertTrue(key in export_dict)
self.assertEqual(self.backup[key], export_dict[key])
def test_import_record(self):
export_string = self.driver.export_record(self.backup)
imported_backup = self.driver.import_record(export_string)
# Make sure we don't lose data when converting from string
for key in _backup_db_fields:
self.assertTrue(key in imported_backup)
self.assertEqual(imported_backup[key], self.backup[key])
def test_verify(self):
self.assertRaises(NotImplementedError,
self.driver.verify, self.backup)
def tearDown(self):
super(BackupBaseDriverTestCase, self).tearDown()

View File

@ -52,6 +52,8 @@
"backup:get": [],
"backup:get_all": [],
"backup:restore": [],
"backup:backup-import": [["rule:admin_api"]],
"backup:backup-export": [["rule:admin_api"]],
"snapshot_extension:snapshot_actions:update_snapshot_status": []
}