Support create volume from backup

This patch implements the spec of creating volume from backup.

Change-Id: Icdc6c7606c43243a9e12d7a42df293b729f589e5
Partial-Implements: blueprint support-create-volume-from-backup
This commit is contained in:
luqitao 2017-10-17 14:13:12 +08:00 committed by TommyLike
parent b6c5f82b7f
commit 39694623e4
25 changed files with 397 additions and 50 deletions

View File

@ -555,6 +555,13 @@ backup_gigabytes_usage:
in: body in: body
required: true required: true
type: object type: object
backup_id_1:
description: |
The UUID of the backup.
in: body
required: false
type: string
min_version: 3.46
backup_service: backup_service:
description: | description: |
The service used to perform the backup. The service used to perform the backup.

View File

@ -6,6 +6,7 @@
"description": null, "description": null,
"multiattach ": false, "multiattach ": false,
"snapshot_id": null, "snapshot_id": null,
"backup_id": null,
"name": null, "name": null,
"imageRef": null, "imageRef": null,
"volume_type": null, "volume_type": null,

View File

@ -188,6 +188,7 @@ Request
- description: description - description: description
- multiattach: multiattach - multiattach: multiattach
- snapshot_id: snapshot_id - snapshot_id: snapshot_id
- backup_id: backup_id_1
- name: name_13 - name: name_13
- imageRef: imageRef - imageRef: imageRef
- volume_type: volume_type - volume_type: volume_type

View File

@ -131,6 +131,8 @@ SUPPORT_COUNT_INFO = '3.45'
SUPPORT_NOVA_IMAGE = '3.46' SUPPORT_NOVA_IMAGE = '3.46'
VOLUME_CREATE_FROM_BACKUP = '3.47'
def get_mv_header(version): def get_mv_header(version):
"""Gets a formatted HTTP microversion header. """Gets a formatted HTTP microversion header.

View File

@ -110,6 +110,7 @@ REST_API_VERSION_HISTORY = """
* 3.45 - Add ``count`` field to volume, backup and snapshot list and * 3.45 - Add ``count`` field to volume, backup and snapshot list and
detail APIs. detail APIs.
* 3.46 - Support create volume by Nova specific image (0 size image). * 3.46 - Support create volume by Nova specific image (0 size image).
* 3.47 - Support create volume from backup.
""" """
# The minimum and maximum versions of the API supported # The minimum and maximum versions of the API supported
@ -117,7 +118,7 @@ REST_API_VERSION_HISTORY = """
# minimum version of the API supported. # minimum version of the API supported.
# Explicitly using /v2 endpoints will still work # Explicitly using /v2 endpoints will still work
_MIN_API_VERSION = "3.0" _MIN_API_VERSION = "3.0"
_MAX_API_VERSION = "3.46" _MAX_API_VERSION = "3.47"
_LEGACY_API_VERSION2 = "2.0" _LEGACY_API_VERSION2 = "2.0"
UPDATED = "2017-09-19T20:18:14Z" UPDATED = "2017-09-19T20:18:14Z"

View File

@ -381,3 +381,7 @@ user documentation.
3.46 3.46
---- ----
Support create volume by Nova specific image (0 size image). Support create volume by Nova specific image (0 size image).
3.47
----
Support create volume from backup.

View File

@ -25,6 +25,7 @@ from cinder.api import microversions as mv
from cinder.api.openstack import wsgi from cinder.api.openstack import wsgi
from cinder.api.v2 import volumes as volumes_v2 from cinder.api.v2 import volumes as volumes_v2
from cinder.api.v3.views import volumes as volume_views_v3 from cinder.api.v3.views import volumes as volume_views_v3
from cinder.backup import api as backup_api
from cinder import exception from cinder import exception
from cinder import group as group_api from cinder import group as group_api
from cinder.i18n import _ from cinder.i18n import _
@ -43,6 +44,7 @@ class VolumeController(volumes_v2.VolumeController):
def __init__(self, ext_mgr): def __init__(self, ext_mgr):
self.group_api = group_api.API() self.group_api = group_api.API()
self.backup_api = backup_api.API()
super(VolumeController, self).__init__(ext_mgr) super(VolumeController, self).__init__(ext_mgr)
def delete(self, req, id): def delete(self, req, id):
@ -334,11 +336,26 @@ class VolumeController(volumes_v2.VolumeController):
else: else:
kwargs['image_id'] = image_uuid kwargs['image_id'] = image_uuid
# Add backup if min version is greater than or equal
# to VOLUME_CREATE_FROM_BACKUP.
if req_version.matches(mv.VOLUME_CREATE_FROM_BACKUP, None):
backup_id = volume.get('backup_id')
if backup_id:
if not uuidutils.is_uuid_like(backup_id):
msg = _("Backup ID must be in UUID form.")
raise exc.HTTPBadRequest(explanation=msg)
kwargs['backup'] = self.backup_api.get(context,
backup_id=backup_id)
else:
kwargs['backup'] = None
size = volume.get('size', None) size = volume.get('size', None)
if size is None and kwargs['snapshot'] is not None: if size is None and kwargs['snapshot'] is not None:
size = kwargs['snapshot']['volume_size'] size = kwargs['snapshot']['volume_size']
elif size is None and kwargs['source_volume'] is not None: elif size is None and kwargs['source_volume'] is not None:
size = kwargs['source_volume']['size'] size = kwargs['source_volume']['size']
elif size is None and kwargs.get('backup') is not None:
size = kwargs['backup']['size']
LOG.info("Create volume of %s GB", size) LOG.info("Create volume of %s GB", size)

View File

@ -165,6 +165,9 @@ class API(base.Base):
idx = idx + 1 idx = idx + 1
return None return None
def get_available_backup_service_host(self, host, az):
return self._get_available_backup_service_host(host, az)
def _get_available_backup_service_host(self, host, az): def _get_available_backup_service_host(self, host, az):
"""Return an appropriate backup service host.""" """Return an appropriate backup service host."""
backup_host = None backup_host = None

View File

@ -139,6 +139,7 @@ OBJ_VERSIONS.add('1.28', {'Service': '1.5'})
OBJ_VERSIONS.add('1.29', {'Service': '1.6'}) OBJ_VERSIONS.add('1.29', {'Service': '1.6'})
OBJ_VERSIONS.add('1.30', {'RequestSpec': '1.2'}) OBJ_VERSIONS.add('1.30', {'RequestSpec': '1.2'})
OBJ_VERSIONS.add('1.31', {'Volume': '1.7'}) OBJ_VERSIONS.add('1.31', {'Volume': '1.7'})
OBJ_VERSIONS.add('1.32', {'RequestSpec': '1.3'})
class CinderObjectRegistry(base.VersionedObjectRegistry): class CinderObjectRegistry(base.VersionedObjectRegistry):

View File

@ -25,7 +25,8 @@ class RequestSpec(base.CinderObject, base.CinderObjectDictCompat,
# Version 1.0: Initial version # Version 1.0: Initial version
# Version 1.1: Added group_id and group_backend # Version 1.1: Added group_id and group_backend
# Version 1.2 Added ``resource_backend`` # Version 1.2 Added ``resource_backend``
VERSION = '1.2' # Version 1.3: Added backup_id
VERSION = '1.3'
fields = { fields = {
'consistencygroup_id': fields.UUIDField(nullable=True), 'consistencygroup_id': fields.UUIDField(nullable=True),
@ -42,7 +43,8 @@ class RequestSpec(base.CinderObject, base.CinderObjectDictCompat,
nullable=True), nullable=True),
'CG_backend': fields.StringField(nullable=True), 'CG_backend': fields.StringField(nullable=True),
'group_backend': fields.StringField(nullable=True), 'group_backend': fields.StringField(nullable=True),
'resource_backend': fields.StringField(nullable=True) 'resource_backend': fields.StringField(nullable=True),
'backup_id': fields.UUIDField(nullable=True),
} }
obj_extra_fields = ['resource_properties'] obj_extra_fields = ['resource_properties']

View File

@ -40,7 +40,7 @@ class ExtractSchedulerSpecTask(flow_utils.CinderTask):
super(ExtractSchedulerSpecTask, self).__init__(addons=[ACTION], super(ExtractSchedulerSpecTask, self).__init__(addons=[ACTION],
**kwargs) **kwargs)
def _populate_request_spec(self, volume, snapshot_id, image_id): def _populate_request_spec(self, volume, snapshot_id, image_id, backup_id):
# Create the full request spec using the volume object. # Create the full request spec using the volume object.
# #
# NOTE(dulek): At this point, a volume can be deleted before it gets # NOTE(dulek): At this point, a volume can be deleted before it gets
@ -53,6 +53,7 @@ class ExtractSchedulerSpecTask(flow_utils.CinderTask):
'volume_id': volume.id, 'volume_id': volume.id,
'snapshot_id': snapshot_id, 'snapshot_id': snapshot_id,
'image_id': image_id, 'image_id': image_id,
'backup_id': backup_id,
'volume_properties': { 'volume_properties': {
'size': utils.as_int(volume.size, quiet=False), 'size': utils.as_int(volume.size, quiet=False),
'availability_zone': volume.availability_zone, 'availability_zone': volume.availability_zone,
@ -62,11 +63,12 @@ class ExtractSchedulerSpecTask(flow_utils.CinderTask):
} }
def execute(self, context, request_spec, volume, snapshot_id, def execute(self, context, request_spec, volume, snapshot_id,
image_id): image_id, backup_id):
# For RPC version < 1.2 backward compatibility # For RPC version < 1.2 backward compatibility
if request_spec is None: if request_spec is None:
request_spec = self._populate_request_spec(volume, request_spec = self._populate_request_spec(volume,
snapshot_id, image_id) snapshot_id, image_id,
backup_id)
return { return {
'request_spec': request_spec, 'request_spec': request_spec,
} }
@ -140,7 +142,7 @@ class ScheduleCreateVolumeTask(flow_utils.CinderTask):
def get_flow(context, driver_api, request_spec=None, def get_flow(context, driver_api, request_spec=None,
filter_properties=None, filter_properties=None,
volume=None, snapshot_id=None, image_id=None): volume=None, snapshot_id=None, image_id=None, backup_id=None):
"""Constructs and returns the scheduler entrypoint flow. """Constructs and returns the scheduler entrypoint flow.
@ -158,6 +160,7 @@ def get_flow(context, driver_api, request_spec=None,
'volume': volume, 'volume': volume,
'snapshot_id': snapshot_id, 'snapshot_id': snapshot_id,
'image_id': image_id, 'image_id': image_id,
'backup_id': backup_id,
} }
flow_name = ACTION.replace(":", "_") + "_scheduler" flow_name = ACTION.replace(":", "_") + "_scheduler"

View File

@ -173,7 +173,8 @@ class SchedulerManager(manager.CleanableManager, manager.Manager):
@objects.Volume.set_workers @objects.Volume.set_workers
def create_volume(self, context, volume, snapshot_id=None, image_id=None, def create_volume(self, context, volume, snapshot_id=None, image_id=None,
request_spec=None, filter_properties=None): request_spec=None, filter_properties=None,
backup_id=None):
self._wait_for_scheduler() self._wait_for_scheduler()
try: try:
@ -183,7 +184,8 @@ class SchedulerManager(manager.CleanableManager, manager.Manager):
filter_properties, filter_properties,
volume, volume,
snapshot_id, snapshot_id,
image_id) image_id,
backup_id)
except Exception: except Exception:
msg = _("Failed to create scheduler manager volume flow") msg = _("Failed to create scheduler manager volume flow")
LOG.exception(msg) LOG.exception(msg)

View File

@ -70,9 +70,10 @@ class SchedulerAPI(rpc.RPCAPI):
3.7 - Adds set_log_levels and get_log_levels 3.7 - Adds set_log_levels and get_log_levels
3.8 - Addds ``valid_host_capacity`` method 3.8 - Addds ``valid_host_capacity`` method
3.9 - Adds create_snapshot method 3.9 - Adds create_snapshot method
3.10 - Adds backup_id to create_volume method.
""" """
RPC_API_VERSION = '3.9' RPC_API_VERSION = '3.10'
RPC_DEFAULT_VERSION = '3.0' RPC_DEFAULT_VERSION = '3.0'
TOPIC = constants.SCHEDULER_TOPIC TOPIC = constants.SCHEDULER_TOPIC
BINARY = 'cinder-scheduler' BINARY = 'cinder-scheduler'
@ -94,12 +95,16 @@ class SchedulerAPI(rpc.RPCAPI):
cctxt.cast(ctxt, 'create_group', **msg_args) cctxt.cast(ctxt, 'create_group', **msg_args)
def create_volume(self, ctxt, volume, snapshot_id=None, image_id=None, def create_volume(self, ctxt, volume, snapshot_id=None, image_id=None,
request_spec=None, filter_properties=None): request_spec=None, filter_properties=None,
backup_id=None):
volume.create_worker() volume.create_worker()
cctxt = self._get_cctxt() cctxt = self._get_cctxt()
msg_args = {'snapshot_id': snapshot_id, 'image_id': image_id, msg_args = {'snapshot_id': snapshot_id, 'image_id': image_id,
'request_spec': request_spec, 'request_spec': request_spec,
'filter_properties': filter_properties, 'volume': volume} 'filter_properties': filter_properties,
'volume': volume, 'backup_id': backup_id}
if not self.client.can_send_version('3.10'):
msg_args.pop('backup_id')
return cctxt.cast(ctxt, 'create_volume', **msg_args) return cctxt.cast(ctxt, 'create_volume', **msg_args)
@rpc.assert_min_rpc_version('3.8') @rpc.assert_min_rpc_version('3.8')

View File

@ -205,6 +205,26 @@ def fake_snapshot(id, **kwargs):
return snapshot return snapshot
def fake_backup(id, **kwargs):
backup = {'id': fake.BACKUP_ID,
'volume_id': fake.VOLUME_ID,
'status': fields.BackupStatus.CREATING,
'size': 1,
'display_name': 'fake_name',
'display_description': 'fake_description',
'user_id': fake.USER_ID,
'project_id': fake.PROJECT_ID,
'temp_volume_id': None,
'temp_snapshot_id': None,
'snapshot_id': None,
'data_timestamp': None,
'restore_volume_id': None,
'backup_metadata': {}}
backup.update(kwargs)
return backup
def fake_snapshot_get_all(context, filters=None, marker=None, limit=None, def fake_snapshot_get_all(context, filters=None, marker=None, limit=None,
sort_keys=None, sort_dirs=None, offset=None): sort_keys=None, sort_dirs=None, offset=None):
return [fake_snapshot(fake.VOLUME_ID, project_id=fake.PROJECT_ID), return [fake_snapshot(fake.VOLUME_ID, project_id=fake.PROJECT_ID),
@ -239,6 +259,13 @@ def fake_snapshot_get(self, context, snapshot_id):
return fake_snapshot(snapshot_id) return fake_snapshot(snapshot_id)
def fake_backup_get(self, context, backup_id):
if backup_id == fake.WILL_NOT_BE_FOUND_ID:
raise exc.BackupNotFound(backup_id=backup_id)
return fake_backup(backup_id)
def fake_consistencygroup_get_notfound(self, context, cg_id): def fake_consistencygroup_get_notfound(self, context, cg_id):
raise exc.GroupNotFound(group_id=cg_id) raise exc.GroupNotFound(group_id=cg_id)

View File

@ -21,8 +21,11 @@ from cinder.api import extensions
from cinder.api import microversions as mv from cinder.api import microversions as mv
from cinder.api.v3 import volume_metadata from cinder.api.v3 import volume_metadata
from cinder.api.v3 import volumes from cinder.api.v3 import volumes
from cinder.backup import rpcapi as backup_rpcapi
from cinder import db from cinder import db
from cinder import exception from cinder import exception
from cinder.objects import base as obj_base
from cinder.scheduler import rpcapi as scheduler_rpcapi
from cinder import test from cinder import test
from cinder.tests.unit.api import fakes from cinder.tests.unit.api import fakes
from cinder.tests.unit.api.v2 import fakes as v2_fakes from cinder.tests.unit.api.v2 import fakes as v2_fakes
@ -135,6 +138,19 @@ class VolumeMetaDataTest(test.TestCase):
self.ext_mgr = extensions.ExtensionManager() self.ext_mgr = extensions.ExtensionManager()
self.ext_mgr.extensions = {} self.ext_mgr.extensions = {}
self.patch(
'cinder.objects.Service.get_minimum_obj_version',
return_value=obj_base.OBJ_VERSIONS.get_current())
def _get_minimum_rpc_version_mock(ctxt, binary):
binary_map = {
'cinder-backup': backup_rpcapi.BackupAPI,
'cinder-scheduler': scheduler_rpcapi.SchedulerAPI,
}
return binary_map[binary].RPC_API_VERSION
self.patch('cinder.objects.Service.get_minimum_rpc_version',
side_effect=_get_minimum_rpc_version_mock)
self.volume_controller = volumes.VolumeController(self.ext_mgr) self.volume_controller = volumes.VolumeController(self.ext_mgr)
self.controller = volume_metadata.Controller() self.controller = volume_metadata.Controller()
self.req_id = str(uuid.uuid4()) self.req_id = str(uuid.uuid4())
@ -261,6 +277,19 @@ class VolumeMetaDataTestNoMicroversion(v2_test.VolumeMetaDataTest):
def setUp(self): def setUp(self):
super(VolumeMetaDataTestNoMicroversion, self).setUp() super(VolumeMetaDataTestNoMicroversion, self).setUp()
self.patch(
'cinder.objects.Service.get_minimum_obj_version',
return_value=obj_base.OBJ_VERSIONS.get_current())
def _get_minimum_rpc_version_mock(ctxt, binary):
binary_map = {
'cinder-backup': backup_rpcapi.BackupAPI,
'cinder-scheduler': scheduler_rpcapi.SchedulerAPI,
}
return binary_map[binary].RPC_API_VERSION
self.patch('cinder.objects.Service.get_minimum_rpc_version',
side_effect=_get_minimum_rpc_version_mock)
self.volume_controller = volumes.VolumeController(self.ext_mgr) self.volume_controller = volumes.VolumeController(self.ext_mgr)
self.controller = volume_metadata.Controller() self.controller = volume_metadata.Controller()
self.url = '/v3/%s/volumes/%s/metadata' % ( self.url = '/v3/%s/volumes/%s/metadata' % (

View File

@ -23,6 +23,7 @@ from cinder.api import extensions
from cinder.api import microversions as mv from cinder.api import microversions as mv
from cinder.api.v2.views.volumes import ViewBuilder from cinder.api.v2.views.volumes import ViewBuilder
from cinder.api.v3 import volumes from cinder.api.v3 import volumes
from cinder.backup import api as backup_api
from cinder import context from cinder import context
from cinder import db from cinder import db
from cinder import exception from cinder import exception
@ -393,7 +394,8 @@ class VolumeApiTest(test.TestCase):
volume_type=None, volume_type=None,
image_ref=None, image_ref=None,
image_id=None, image_id=None,
group_id=None): group_id=None,
backup_id=None):
vol = {"size": size, vol = {"size": size,
"name": name, "name": name,
"description": description, "description": description,
@ -409,6 +411,8 @@ class VolumeApiTest(test.TestCase):
vol['image_id'] = image_id vol['image_id'] = image_id
elif image_ref is not None: elif image_ref is not None:
vol['imageRef'] = image_ref vol['imageRef'] = image_ref
elif backup_id is not None:
vol['backup_id'] = backup_id
return vol return vol
@ -557,6 +561,42 @@ class VolumeApiTest(test.TestCase):
v2_fakes.DEFAULT_VOL_DESCRIPTION, v2_fakes.DEFAULT_VOL_DESCRIPTION,
**kwargs) **kwargs)
@ddt.data(mv.VOLUME_CREATE_FROM_BACKUP,
mv.get_prior_version(mv.VOLUME_CREATE_FROM_BACKUP))
@mock.patch.object(db.sqlalchemy.api, '_volume_type_get_full',
autospec=True)
@mock.patch.object(backup_api.API, 'get', autospec=True)
@mock.patch.object(volume_api.API, 'create', autospec=True)
def test_volume_creation_from_backup(self, max_ver, create, get_backup,
volume_type_get):
create.side_effect = v2_fakes.fake_volume_api_create
get_backup.side_effect = v2_fakes.fake_backup_get
volume_type_get.side_effect = v2_fakes.fake_volume_type_get
backup_id = fake.BACKUP_ID
vol = self._vol_in_request_body(backup_id=backup_id)
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v3/volumes')
req.api_version_request = mv.get_api_version(max_ver)
res_dict = self.controller.create(req, body)
ex = self._expected_vol_from_controller(
req_version=req.api_version_request)
self.assertEqual(ex, res_dict)
context = req.environ['cinder.context']
kwargs = self._expected_volume_api_create_kwargs(
req_version=req.api_version_request)
if max_ver >= mv.VOLUME_CREATE_FROM_BACKUP:
get_backup.assert_called_once_with(self.controller.backup_api,
context, backup_id)
kwargs.update({'backup': v2_fakes.fake_backup_get(None, context,
backup_id)})
create.assert_called_once_with(self.controller.volume_api, context,
vol['size'],
v2_fakes.DEFAULT_VOL_NAME,
v2_fakes.DEFAULT_VOL_DESCRIPTION,
**kwargs)
@ddt.data({'s': 'ea895e29-8485-4930-bbb8-c5616a309c0e'}, @ddt.data({'s': 'ea895e29-8485-4930-bbb8-c5616a309c0e'},
['ea895e29-8485-4930-bbb8-c5616a309c0e'], ['ea895e29-8485-4930-bbb8-c5616a309c0e'],
42) 42)

View File

@ -42,7 +42,7 @@ object_data = {
'ManageableVolumeList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e', 'ManageableVolumeList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e',
'QualityOfServiceSpecs': '1.0-0b212e0a86ee99092229874e03207fe8', 'QualityOfServiceSpecs': '1.0-0b212e0a86ee99092229874e03207fe8',
'QualityOfServiceSpecsList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e', 'QualityOfServiceSpecsList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e',
'RequestSpec': '1.2-207502df46a50575a818076e1ea119db', 'RequestSpec': '1.3-9510bf37e30fd4c282599a4b2a26675e',
'Service': '1.6-e881b6b324151dd861e09cdfffcdaccd', 'Service': '1.6-e881b6b324151dd861e09cdfffcdaccd',
'ServiceList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e', 'ServiceList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e',
'Snapshot': '1.5-ac1cdbd5b89588f6a8f44afdf6b8b201', 'Snapshot': '1.5-ac1cdbd5b89588f6a8f44afdf6b8b201',

View File

@ -62,7 +62,10 @@ class SchedulerRPCAPITestCase(test.RPCAPITestCase):
timestamp='123') timestamp='123')
can_send_version.assert_called_once_with('3.3') can_send_version.assert_called_once_with('3.3')
def test_create_volume(self): @ddt.data('3.0', '3.10')
@mock.patch('oslo_messaging.RPCClient.can_send_version')
def test_create_volume(self, version, can_send_version):
can_send_version.side_effect = lambda x: x == version
create_worker_mock = self.mock_object(self.fake_volume, create_worker_mock = self.mock_object(self.fake_volume,
'create_worker') 'create_worker')
self._test_rpc_api('create_volume', self._test_rpc_api('create_volume',
@ -70,9 +73,11 @@ class SchedulerRPCAPITestCase(test.RPCAPITestCase):
volume=self.fake_volume, volume=self.fake_volume,
snapshot_id=fake_constants.SNAPSHOT_ID, snapshot_id=fake_constants.SNAPSHOT_ID,
image_id=fake_constants.IMAGE_ID, image_id=fake_constants.IMAGE_ID,
backup_id=fake_constants.BACKUP_ID,
request_spec=self.fake_rs_obj, request_spec=self.fake_rs_obj,
filter_properties=self.fake_fp_dict) filter_properties=self.fake_fp_dict)
create_worker_mock.assert_called_once() create_worker_mock.assert_called_once()
can_send_version.assert_called_once_with('3.10')
@mock.patch('oslo_messaging.RPCClient.can_send_version', @mock.patch('oslo_messaging.RPCClient.can_send_version',
return_value=True) return_value=True)

View File

@ -35,7 +35,8 @@ class FakeSchedulerRpcAPI(object):
self.test_inst = test_inst self.test_inst = test_inst
def create_volume(self, ctxt, volume, snapshot_id=None, image_id=None, def create_volume(self, ctxt, volume, snapshot_id=None, image_id=None,
request_spec=None, filter_properties=None): request_spec=None, filter_properties=None,
backup_id=None):
self.test_inst.assertEqual(self.expected_spec, request_spec) self.test_inst.assertEqual(self.expected_spec, request_spec)

View File

@ -27,6 +27,7 @@ from cinder import context
from cinder import exception from cinder import exception
from cinder.message import message_field from cinder.message import message_field
from cinder import test from cinder import test
from cinder.tests.unit.backup import fake_backup
from cinder.tests.unit.consistencygroup import fake_consistencygroup from cinder.tests.unit.consistencygroup import fake_consistencygroup
from cinder.tests.unit import fake_constants as fakes from cinder.tests.unit import fake_constants as fakes
from cinder.tests.unit import fake_snapshot from cinder.tests.unit import fake_snapshot
@ -75,7 +76,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
'image_id': 4, 'image_id': 4,
'consistencygroup_id': None, 'consistencygroup_id': None,
'cgsnapshot_id': None, 'cgsnapshot_id': None,
'group_id': None, } 'group_id': None,
'backup_id': None, }
# Fake objects assert specs # Fake objects assert specs
task = create_volume.VolumeCastTask( task = create_volume.VolumeCastTask(
@ -97,7 +99,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
'image_id': 4, 'image_id': 4,
'consistencygroup_id': None, 'consistencygroup_id': None,
'cgsnapshot_id': None, 'cgsnapshot_id': None,
'group_id': None, } 'group_id': None,
'backup_id': None, }
# Fake objects assert specs # Fake objects assert specs
task = create_volume.VolumeCastTask( task = create_volume.VolumeCastTask(
@ -131,7 +134,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
'image_id': None, 'image_id': None,
'consistencygroup_id': None, 'consistencygroup_id': None,
'cgsnapshot_id': None, 'cgsnapshot_id': None,
'group_id': None, } 'group_id': None,
'backup_id': None, }
# Fake objects assert specs # Fake objects assert specs
task = create_volume.VolumeCastTask( task = create_volume.VolumeCastTask(
@ -148,7 +152,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
'image_id': 4, 'image_id': 4,
'consistencygroup_id': 5, 'consistencygroup_id': 5,
'cgsnapshot_id': None, 'cgsnapshot_id': None,
'group_id': None, } 'group_id': None,
'backup_id': None, }
# Fake objects assert specs # Fake objects assert specs
task = create_volume.VolumeCastTask( task = create_volume.VolumeCastTask(
@ -192,7 +197,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
consistencygroup=None, consistencygroup=None,
cgsnapshot=None, cgsnapshot=None,
group=None, group=None,
group_snapshot=None) group_snapshot=None,
backup=None)
self.assertEqual(replication_status, result['replication_status'], self.assertEqual(replication_status, result['replication_status'],
extra_specs) extra_specs)
@ -238,7 +244,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
consistencygroup=None, consistencygroup=None,
cgsnapshot=None, cgsnapshot=None,
group=None, group=None,
group_snapshot=None) group_snapshot=None,
backup=None)
fake_get_encryption_key.assert_called_once_with( fake_get_encryption_key.assert_called_once_with(
fake_key_manager, self.ctxt, fakes.VOLUME_TYPE_ID, fake_key_manager, self.ctxt, fakes.VOLUME_TYPE_ID,
None, None, image_meta) None, None, image_meta)
@ -283,7 +290,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
consistencygroup=None, consistencygroup=None,
cgsnapshot=None, cgsnapshot=None,
group=None, group=None,
group_snapshot=None) group_snapshot=None,
backup=None)
expected_result = {'size': 1, expected_result = {'size': 1,
'snapshot_id': None, 'snapshot_id': None,
'source_volid': None, 'source_volid': None,
@ -296,7 +304,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
'cgsnapshot_id': None, 'cgsnapshot_id': None,
'group_id': None, 'group_id': None,
'refresh_az': False, 'refresh_az': False,
'replication_status': 'disabled'} 'replication_status': 'disabled',
'backup_id': None}
self.assertEqual(expected_result, result) self.assertEqual(expected_result, result)
@mock.patch('cinder.volume.volume_types.is_encrypted') @mock.patch('cinder.volume.volume_types.is_encrypted')
@ -340,7 +349,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
consistencygroup=None, consistencygroup=None,
cgsnapshot=None, cgsnapshot=None,
group=None, group=None,
group_snapshot=None) group_snapshot=None,
backup=None)
@mock.patch('cinder.volume.volume_types.is_encrypted') @mock.patch('cinder.volume.volume_types.is_encrypted')
@mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs') @mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs')
@ -384,7 +394,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
consistencygroup=None, consistencygroup=None,
cgsnapshot=None, cgsnapshot=None,
group=None, group=None,
group_snapshot=None) group_snapshot=None,
backup=None)
expected_result = {'size': 1, expected_result = {'size': 1,
'snapshot_id': None, 'snapshot_id': None,
'source_volid': None, 'source_volid': None,
@ -397,7 +408,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
'cgsnapshot_id': None, 'cgsnapshot_id': None,
'group_id': None, 'group_id': None,
'refresh_az': True, 'refresh_az': True,
'replication_status': 'disabled'} 'replication_status': 'disabled',
'backup_id': None}
self.assertEqual(expected_result, result) self.assertEqual(expected_result, result)
@mock.patch('cinder.volume.volume_types.is_encrypted', @mock.patch('cinder.volume.volume_types.is_encrypted',
@ -448,7 +460,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
consistencygroup=None, consistencygroup=None,
cgsnapshot=None, cgsnapshot=None,
group=None, group=None,
group_snapshot=None) group_snapshot=None,
backup=None)
mock_is_encrypted.assert_called_once_with(self.ctxt, 1) mock_is_encrypted.assert_called_once_with(self.ctxt, 1)
mock_get_volume_type_encryption.assert_called_once_with(self.ctxt, 1) mock_get_volume_type_encryption.assert_called_once_with(self.ctxt, 1)
@ -492,7 +505,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
consistencygroup=None, consistencygroup=None,
cgsnapshot=None, cgsnapshot=None,
group=None, group=None,
group_snapshot=None) group_snapshot=None,
backup=None)
expected_result = {'size': (sys.maxsize + 1), expected_result = {'size': (sys.maxsize + 1),
'snapshot_id': None, 'snapshot_id': None,
'source_volid': None, 'source_volid': None,
@ -505,7 +519,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
'consistencygroup_id': None, 'consistencygroup_id': None,
'cgsnapshot_id': None, 'cgsnapshot_id': None,
'refresh_az': False, 'refresh_az': False,
'group_id': None, } 'group_id': None,
'backup_id': None}
self.assertEqual(expected_result, result) self.assertEqual(expected_result, result)
@mock.patch('cinder.volume.volume_types.is_encrypted') @mock.patch('cinder.volume.volume_types.is_encrypted')
@ -549,7 +564,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
consistencygroup=None, consistencygroup=None,
cgsnapshot=None, cgsnapshot=None,
group=None, group=None,
group_snapshot=None) group_snapshot=None,
backup=None)
expected_result = {'size': 1, expected_result = {'size': 1,
'snapshot_id': None, 'snapshot_id': None,
'source_volid': None, 'source_volid': None,
@ -562,7 +578,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
'cgsnapshot_id': None, 'cgsnapshot_id': None,
'group_id': None, 'group_id': None,
'refresh_az': False, 'refresh_az': False,
'replication_status': 'disabled'} 'replication_status': 'disabled',
'backup_id': None}
self.assertEqual(expected_result, result) self.assertEqual(expected_result, result)
@mock.patch('cinder.volume.volume_types.is_encrypted') @mock.patch('cinder.volume.volume_types.is_encrypted')
@ -613,7 +630,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
consistencygroup=None, consistencygroup=None,
cgsnapshot=None, cgsnapshot=None,
group=None, group=None,
group_snapshot=None) group_snapshot=None,
backup=None)
expected_result = {'size': 1, expected_result = {'size': 1,
'snapshot_id': None, 'snapshot_id': None,
'source_volid': None, 'source_volid': None,
@ -626,7 +644,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
'cgsnapshot_id': None, 'cgsnapshot_id': None,
'group_id': None, 'group_id': None,
'refresh_az': False, 'refresh_az': False,
'replication_status': 'disabled'} 'replication_status': 'disabled',
'backup_id': None}
self.assertEqual(expected_result, result) self.assertEqual(expected_result, result)
@mock.patch('cinder.db.volume_type_get_by_name') @mock.patch('cinder.db.volume_type_get_by_name')
@ -678,7 +697,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
consistencygroup=None, consistencygroup=None,
cgsnapshot=None, cgsnapshot=None,
group=None, group=None,
group_snapshot=None) group_snapshot=None,
backup=None)
expected_result = {'size': 1, expected_result = {'size': 1,
'snapshot_id': None, 'snapshot_id': None,
'source_volid': None, 'source_volid': None,
@ -691,7 +711,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
'cgsnapshot_id': None, 'cgsnapshot_id': None,
'group_id': None, 'group_id': None,
'refresh_az': False, 'refresh_az': False,
'replication_status': 'disabled'} 'replication_status': 'disabled',
'backup_id': None}
self.assertEqual(expected_result, result) self.assertEqual(expected_result, result)
@mock.patch('cinder.db.volume_type_get_by_name') @mock.patch('cinder.db.volume_type_get_by_name')
@ -742,7 +763,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
consistencygroup=None, consistencygroup=None,
cgsnapshot=None, cgsnapshot=None,
group=None, group=None,
group_snapshot=None) group_snapshot=None,
backup=None)
expected_result = {'size': 1, expected_result = {'size': 1,
'snapshot_id': None, 'snapshot_id': None,
'source_volid': None, 'source_volid': None,
@ -755,7 +777,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
'cgsnapshot_id': None, 'cgsnapshot_id': None,
'group_id': None, 'group_id': None,
'refresh_az': False, 'refresh_az': False,
'replication_status': 'disabled'} 'replication_status': 'disabled',
'backup_id': None}
self.assertEqual(expected_result, result) self.assertEqual(expected_result, result)
@mock.patch('cinder.db.volume_type_get_by_name') @mock.patch('cinder.db.volume_type_get_by_name')
@ -804,7 +827,8 @@ class CreateVolumeFlowTestCase(test.TestCase):
consistencygroup=None, consistencygroup=None,
cgsnapshot=None, cgsnapshot=None,
group=None, group=None,
group_snapshot=None) group_snapshot=None,
backup=None)
@ddt.ddt @ddt.ddt
@ -986,6 +1010,67 @@ class CreateVolumeFlowManagerTestCase(test.TestCase):
fake_driver.copy_image_to_volume.assert_called_once_with( fake_driver.copy_image_to_volume.assert_called_once_with(
self.ctxt, volume, fake_image_service, image_id) self.ctxt, volume, fake_image_service, image_id)
@ddt.data({'driver_error': True},
{'driver_error': False})
@mock.patch('cinder.backup.api.API.get_available_backup_service_host')
@mock.patch('cinder.backup.rpcapi.BackupAPI.restore_backup')
@mock.patch('oslo_service.loopingcall.'
'FixedIntervalWithTimeoutLoopingCall')
@mock.patch('cinder.volume.flows.manager.create_volume.'
'CreateVolumeFromSpecTask.'
'_create_raw_volume')
@mock.patch('cinder.db.volume_update')
@mock.patch('cinder.db.backup_update')
@mock.patch('cinder.objects.Volume.get_by_id')
@mock.patch('cinder.objects.Backup.get_by_id')
@ddt.unpack
def test_create_from_backup(self,
backup_get_by_id,
volume_get_by_id,
mock_backup_update,
mock_volume_update,
mock_create_volume,
mock_fixed_looping_call,
mock_restore_backup,
mock_get_backup_host,
driver_error):
fake_db = mock.MagicMock()
fake_driver = mock.MagicMock()
fake_volume_manager = mock.MagicMock()
backup_host = 'host@backend#pool'
fake_manager = create_volume_manager.CreateVolumeFromSpecTask(
fake_volume_manager, fake_db, fake_driver)
volume_obj = fake_volume.fake_volume_obj(self.ctxt)
backup_obj = fake_backup.fake_backup_obj(self.ctxt,
**{'status': 'available',
'host': backup_host})
backup_get_by_id.return_value = backup_obj
volume_get_by_id.return_value = volume_obj
mock_create_volume.return_value = {}
mock_get_backup_host.return_value = backup_host
mock_fixed_looping_call.return_value = mock.MagicMock()
if driver_error:
fake_driver.create_volume_from_backup.side_effect = [
NotImplementedError]
fake_manager._create_from_backup(self.ctxt, volume_obj,
backup_obj.id)
fake_driver.create_volume_from_backup.assert_called_once_with(
volume_obj, backup_obj)
if driver_error:
mock_create_volume.assert_called_once_with(volume_obj)
mock_get_backup_host.assert_called_once_with(
backup_obj.host, backup_obj.availability_zone)
mock_restore_backup.assert_called_once_with(self.ctxt,
backup_host,
backup_obj,
volume_obj['id'])
else:
fake_driver.create_volume_from_backup.assert_called_once_with(
volume_obj, backup_obj)
class CreateVolumeFlowManagerGlanceCinderBackendCase(test.TestCase): class CreateVolumeFlowManagerGlanceCinderBackendCase(test.TestCase):

View File

@ -197,7 +197,8 @@ class API(base.Base):
scheduler_hints=None, scheduler_hints=None,
source_replica=None, consistencygroup=None, source_replica=None, consistencygroup=None,
cgsnapshot=None, multiattach=False, source_cg=None, cgsnapshot=None, multiattach=False, source_cg=None,
group=None, group_snapshot=None, source_group=None): group=None, group_snapshot=None, source_group=None,
backup=None):
context.authorize(vol_policy.CREATE_FROM_IMAGE_POLICY) context.authorize(vol_policy.CREATE_FROM_IMAGE_POLICY)
@ -299,6 +300,7 @@ class API(base.Base):
'group': group, 'group': group,
'group_snapshot': group_snapshot, 'group_snapshot': group_snapshot,
'source_group': source_group, 'source_group': source_group,
'backup': backup,
} }
try: try:
sched_rpcapi = (self.scheduler_rpcapi if ( sched_rpcapi = (self.scheduler_rpcapi if (

View File

@ -1852,6 +1852,19 @@ class BaseVD(object):
def accept_transfer(self, context, volume, new_user, new_project): def accept_transfer(self, context, volume, new_user, new_project):
pass pass
def create_volume_from_backup(self, volume, backup):
"""Creates a volume from a backup.
Can optionally return a Dictionary of changes to the volume object to
be persisted.
:param volume: the volume object to be created.
:param backup: the backup object as source.
:returns: volume_model_update
"""
raise NotImplementedError()
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class CloneableImageVD(object): class CloneableImageVD(object):

View File

@ -49,6 +49,7 @@ REPLICA_PROCEED_STATUS = ('active', 'active-stopped',)
CG_PROCEED_STATUS = ('available', 'creating',) CG_PROCEED_STATUS = ('available', 'creating',)
CGSNAPSHOT_PROCEED_STATUS = ('available',) CGSNAPSHOT_PROCEED_STATUS = ('available',)
GROUP_PROCEED_STATUS = ('available', 'creating',) GROUP_PROCEED_STATUS = ('available', 'creating',)
BACKUP_PROCEED_STATUS = (fields.BackupStatus.AVAILABLE,)
class ExtractVolumeRequestTask(flow_utils.CinderTask): class ExtractVolumeRequestTask(flow_utils.CinderTask):
@ -69,7 +70,7 @@ class ExtractVolumeRequestTask(flow_utils.CinderTask):
'source_volid', 'volume_type', 'volume_type_id', 'source_volid', 'volume_type', 'volume_type_id',
'encryption_key_id', 'consistencygroup_id', 'encryption_key_id', 'consistencygroup_id',
'cgsnapshot_id', 'qos_specs', 'group_id', 'cgsnapshot_id', 'qos_specs', 'group_id',
'refresh_az']) 'refresh_az', 'backup_id'])
def __init__(self, image_service, availability_zones, **kwargs): def __init__(self, image_service, availability_zones, **kwargs):
super(ExtractVolumeRequestTask, self).__init__(addons=[ACTION], super(ExtractVolumeRequestTask, self).__init__(addons=[ACTION],
@ -135,8 +136,13 @@ class ExtractVolumeRequestTask(flow_utils.CinderTask):
return self._extract_resource(source_volume, (SRC_VOL_PROCEED_STATUS,), return self._extract_resource(source_volume, (SRC_VOL_PROCEED_STATUS,),
exception.InvalidVolume, 'source volume') exception.InvalidVolume, 'source volume')
def _extract_backup(self, backup):
return self._extract_resource(backup, (BACKUP_PROCEED_STATUS,),
exception.InvalidBackup,
'backup')
@staticmethod @staticmethod
def _extract_size(size, source_volume, snapshot): def _extract_size(size, source_volume, snapshot, backup):
"""Extracts and validates the volume size. """Extracts and validates the volume size.
This function will validate or when not provided fill in the provided This function will validate or when not provided fill in the provided
@ -162,6 +168,15 @@ class ExtractVolumeRequestTask(flow_utils.CinderTask):
'source_size': source_volume['size']} 'source_size': source_volume['size']}
raise exception.InvalidInput(reason=msg) raise exception.InvalidInput(reason=msg)
def validate_backup_size(size):
if backup and size < backup['size']:
msg = _("Volume size %(size)sGB cannot be smaller than "
"the backup size %(backup_size)sGB. "
"It must be >= backup size.")
msg = msg % {'size': size,
'backup_size': backup['size']}
raise exception.InvalidInput(reason=msg)
def validate_int(size): def validate_int(size):
if not isinstance(size, six.integer_types) or size <= 0: if not isinstance(size, six.integer_types) or size <= 0:
msg = _("Volume size '%(size)s' must be an integer and" msg = _("Volume size '%(size)s' must be an integer and"
@ -175,12 +190,16 @@ class ExtractVolumeRequestTask(flow_utils.CinderTask):
validator_functors.append(validate_source_size) validator_functors.append(validate_source_size)
elif snapshot: elif snapshot:
validator_functors.append(validate_snap_size) validator_functors.append(validate_snap_size)
elif backup:
validator_functors.append(validate_backup_size)
# If the size is not provided then try to provide it. # If the size is not provided then try to provide it.
if not size and source_volume: if not size and source_volume:
size = source_volume['size'] size = source_volume['size']
elif not size and snapshot: elif not size and snapshot:
size = snapshot.volume_size size = snapshot.volume_size
elif not size and backup:
size = backup['size']
size = utils.as_int(size) size = utils.as_int(size)
LOG.debug("Validating volume size '%(size)s' using %(functors)s", LOG.debug("Validating volume size '%(size)s' using %(functors)s",
@ -414,18 +433,20 @@ class ExtractVolumeRequestTask(flow_utils.CinderTask):
def execute(self, context, size, snapshot, image_id, source_volume, def execute(self, context, size, snapshot, image_id, source_volume,
availability_zone, volume_type, metadata, key_manager, availability_zone, volume_type, metadata, key_manager,
consistencygroup, cgsnapshot, group, group_snapshot): consistencygroup, cgsnapshot, group, group_snapshot, backup):
utils.check_exclusive_options(snapshot=snapshot, utils.check_exclusive_options(snapshot=snapshot,
imageRef=image_id, imageRef=image_id,
source_volume=source_volume) source_volume=source_volume,
backup=backup)
context.authorize(policy.CREATE_POLICY) context.authorize(policy.CREATE_POLICY)
# TODO(harlowja): what guarantee is there that the snapshot or source # TODO(harlowja): what guarantee is there that the snapshot or source
# volume will remain available after we do this initial verification?? # volume will remain available after we do this initial verification??
snapshot_id = self._extract_snapshot(snapshot) snapshot_id = self._extract_snapshot(snapshot)
source_volid = self._extract_source_volume(source_volume) source_volid = self._extract_source_volume(source_volume)
size = self._extract_size(size, source_volume, snapshot) backup_id = self._extract_backup(backup)
size = self._extract_size(size, source_volume, snapshot, backup)
consistencygroup_id = self._extract_consistencygroup(consistencygroup) consistencygroup_id = self._extract_consistencygroup(consistencygroup)
cgsnapshot_id = self._extract_cgsnapshot(cgsnapshot) cgsnapshot_id = self._extract_cgsnapshot(cgsnapshot)
group_id = self._extract_group(group) group_id = self._extract_group(group)
@ -491,7 +512,8 @@ class ExtractVolumeRequestTask(flow_utils.CinderTask):
'cgsnapshot_id': cgsnapshot_id, 'cgsnapshot_id': cgsnapshot_id,
'group_id': group_id, 'group_id': group_id,
'replication_status': replication_status, 'replication_status': replication_status,
'refresh_az': refresh_az 'refresh_az': refresh_az,
'backup_id': backup_id,
} }
@ -726,7 +748,7 @@ class VolumeCastTask(flow_utils.CinderTask):
requires = ['image_id', 'scheduler_hints', 'snapshot_id', requires = ['image_id', 'scheduler_hints', 'snapshot_id',
'source_volid', 'volume_id', 'volume', 'volume_type', 'source_volid', 'volume_id', 'volume', 'volume_type',
'volume_properties', 'consistencygroup_id', 'volume_properties', 'consistencygroup_id',
'cgsnapshot_id', 'group_id', ] 'cgsnapshot_id', 'group_id', 'backup_id', ]
super(VolumeCastTask, self).__init__(addons=[ACTION], super(VolumeCastTask, self).__init__(addons=[ACTION],
requires=requires) requires=requires)
self.volume_rpcapi = volume_rpcapi self.volume_rpcapi = volume_rpcapi
@ -740,6 +762,7 @@ class VolumeCastTask(flow_utils.CinderTask):
image_id = request_spec['image_id'] image_id = request_spec['image_id']
cgroup_id = request_spec['consistencygroup_id'] cgroup_id = request_spec['consistencygroup_id']
group_id = request_spec['group_id'] group_id = request_spec['group_id']
backup_id = request_spec['backup_id']
if cgroup_id: if cgroup_id:
# If cgroup_id existed, we should cast volume to the scheduler # If cgroup_id existed, we should cast volume to the scheduler
# to choose a proper pool whose backend is same as CG's backend. # to choose a proper pool whose backend is same as CG's backend.
@ -776,7 +799,8 @@ class VolumeCastTask(flow_utils.CinderTask):
snapshot_id=snapshot_id, snapshot_id=snapshot_id,
image_id=image_id, image_id=image_id,
request_spec=request_spec, request_spec=request_spec,
filter_properties=filter_properties) filter_properties=filter_properties,
backup_id=backup_id)
def execute(self, context, **kwargs): def execute(self, context, **kwargs):
scheduler_hints = kwargs.pop('scheduler_hints', None) scheduler_hints = kwargs.pop('scheduler_hints', None)

View File

@ -22,6 +22,8 @@ import taskflow.engines
from taskflow.patterns import linear_flow from taskflow.patterns import linear_flow
from taskflow.types import failure as ft from taskflow.types import failure as ft
from cinder import backup as backup_api
from cinder.backup import rpcapi as backup_rpcapi
from cinder import context as cinder_context from cinder import context as cinder_context
from cinder import coordination from cinder import coordination
from cinder import exception from cinder import exception
@ -33,6 +35,7 @@ from cinder.message import api as message_api
from cinder.message import message_field from cinder.message import message_field
from cinder import objects from cinder import objects
from cinder.objects import consistencygroup from cinder.objects import consistencygroup
from cinder.objects import fields
from cinder import utils from cinder import utils
from cinder.volume.flows import common from cinder.volume.flows import common
from cinder.volume import utils as volume_utils from cinder.volume import utils as volume_utils
@ -267,7 +270,7 @@ class ExtractVolumeSpecTask(flow_utils.CinderTask):
'status': volume.status, 'status': volume.status,
'type': 'raw', # This will have the type of the volume to be 'type': 'raw', # This will have the type of the volume to be
# created, which should be one of [raw, snap, # created, which should be one of [raw, snap,
# source_vol, image] # source_vol, image, backup]
'volume_id': volume.id, 'volume_id': volume.id,
'volume_name': volume_name, 'volume_name': volume_name,
'volume_size': volume_size, 'volume_size': volume_size,
@ -314,7 +317,17 @@ class ExtractVolumeSpecTask(flow_utils.CinderTask):
# demand in the future. # demand in the future.
'image_service': image_service, 'image_service': image_service,
}) })
elif request_spec.get('backup_id'):
# We are making a backup based volume instead of a raw volume.
specs.update({
'type': 'backup',
'backup_id': request_spec['backup_id'],
# NOTE(luqitao): if the driver does not implement the method
# `create_volume_from_backup`, cinder-backup will update the
# volume's status, otherwise we need update it in the method
# `CreateVolumeOnFinishTask`.
'need_update_volume': True,
})
return specs return specs
def revert(self, context, result, **kwargs): def revert(self, context, result, **kwargs):
@ -357,6 +370,8 @@ class CreateVolumeFromSpecTask(flow_utils.CinderTask):
Reversion strategy: N/A Reversion strategy: N/A
""" """
default_provides = 'volume_spec'
def __init__(self, manager, db, driver, image_volume_cache=None): def __init__(self, manager, db, driver, image_volume_cache=None):
super(CreateVolumeFromSpecTask, self).__init__(addons=[ACTION]) super(CreateVolumeFromSpecTask, self).__init__(addons=[ACTION])
self.manager = manager self.manager = manager
@ -364,6 +379,8 @@ class CreateVolumeFromSpecTask(flow_utils.CinderTask):
self.driver = driver self.driver = driver
self.image_volume_cache = image_volume_cache self.image_volume_cache = image_volume_cache
self.message = message_api.API() self.message = message_api.API()
self.backup_api = backup_api.API()
self.backup_rpcapi = backup_rpcapi.BackupAPI()
def _handle_bootable_volume_glance_meta(self, context, volume, def _handle_bootable_volume_glance_meta(self, context, volume,
**kwargs): **kwargs):
@ -864,6 +881,45 @@ class CreateVolumeFromSpecTask(flow_utils.CinderTask):
image_meta=image_meta) image_meta=image_meta)
return model_update return model_update
def _create_from_backup(self, context, volume, backup_id, **kwargs):
LOG.info("Creating volume %(volume_id)s from backup %(backup_id)s.",
{'volume_id': volume.id,
'backup_id': backup_id})
ret = {}
backup = objects.Backup.get_by_id(context, backup_id)
try:
ret = self.driver.create_volume_from_backup(volume, backup)
need_update_volume = True
except NotImplementedError:
LOG.info("Backend does not support creating volume from "
"backup %(id)s. It will directly create the raw volume "
"at the backend and then schedule the request to the "
"backup service to restore the volume with backup.",
{'id': backup_id})
model_update = self._create_raw_volume(volume, **kwargs) or {}
model_update.update({'status': 'restoring-backup'})
volume.update(model_update)
volume.save()
backup_host = self.backup_api.get_available_backup_service_host(
backup.host, backup.availability_zone)
updates = {'status': fields.BackupStatus.RESTORING,
'restore_volume_id': volume.id,
'host': backup_host}
backup.update(updates)
backup.save()
self.backup_rpcapi.restore_backup(context, backup.host, backup,
volume.id)
need_update_volume = False
LOG.info("Created volume %(volume_id)s from backup %(backup_id)s "
"successfully.",
{'volume_id': volume.id,
'backup_id': backup_id})
return ret, need_update_volume
def _create_raw_volume(self, volume, **kwargs): def _create_raw_volume(self, volume, **kwargs):
try: try:
ret = self.driver.create_volume(volume) ret = self.driver.create_volume(volume)
@ -910,6 +966,10 @@ class CreateVolumeFromSpecTask(flow_utils.CinderTask):
model_update = self._create_from_image(context, model_update = self._create_from_image(context,
volume, volume,
**volume_spec) **volume_spec)
elif create_type == 'backup':
model_update, need_update_volume = self._create_from_backup(
context, volume, **volume_spec)
volume_spec.update({'need_update_volume': need_update_volume})
else: else:
raise exception.VolumeTypeNotFound(volume_type_id=create_type) raise exception.VolumeTypeNotFound(volume_type_id=create_type)
@ -927,6 +987,7 @@ class CreateVolumeFromSpecTask(flow_utils.CinderTask):
"with creation provided model %(model)s", "with creation provided model %(model)s",
{'volume_id': volume_id, 'model': model_update}) {'volume_id': volume_id, 'model': model_update})
raise raise
return volume_spec
def _cleanup_cg_in_volume(self, volume): def _cleanup_cg_in_volume(self, volume):
# NOTE(xyang): Cannot have both group_id and consistencygroup_id. # NOTE(xyang): Cannot have both group_id and consistencygroup_id.
@ -959,6 +1020,11 @@ class CreateVolumeOnFinishTask(NotifyVolumeActionTask):
} }
def execute(self, context, volume, volume_spec): def execute(self, context, volume, volume_spec):
need_update_volume = volume_spec.pop('need_update_volume', True)
if not need_update_volume:
super(CreateVolumeOnFinishTask, self).execute(context, volume)
return
new_status = self.status_translation.get(volume_spec.get('status'), new_status = self.status_translation.get(volume_spec.get('status'),
'available') 'available')
update = { update = {

View File

@ -0,0 +1,6 @@
---
features:
- |
Starting with API microversion 3.47, Cinder now supports the ability to
create a volume directly from a backup. For instance, you can use the
command: ``cinder create <size> --backup-id <backup_id>`` in cinderclient.