Implement ability to Clone volumes in Cinder.

This implements the capability to create usable volume clones in Cinder,
for the LVM case we create a temporary snapshot to copy from so that
volumes can remain attached during cloning.  This works by passing
in a source-volume-id to the create command (similar to create-from-snapshot).

Currently we limit clone to the same Cinder node, and only for the base LVM driver.
All other drivers should raise NotImplemented, most inherit from the SANISCSIDriver,
so move the function there and raise until we have a general implementation
for SANISCSI based drivers.

Those drivers that inherit from ISCSI directly instead of SANISCSI,
add the function explicitly and raise NotImplementedError there as well.

Implements blueprint add-cloning-support-to-cinder

Change-Id: I72bf90baf22bec2d4806d00e2b827a594ed213f4
This commit is contained in:
John Griffith 2012-12-12 15:23:56 -07:00
parent 18269eaad8
commit d99fb6011c
23 changed files with 232 additions and 19 deletions

View File

@ -98,6 +98,7 @@ def _translate_volume_summary_view(context, vol, image_id=None):
d['volume_type'] = str(vol['volume_type_id'])
d['snapshot_id'] = vol['snapshot_id']
d['source_volid'] = vol['source_volid']
if image_id:
d['image_id'] = image_id
@ -138,6 +139,7 @@ def make_volume(elem):
elem.set('display_description')
elem.set('volume_type')
elem.set('snapshot_id')
elem.set('source_volid')
attachments = xmlutil.SubTemplateElement(elem, 'attachments')
attachment = xmlutil.SubTemplateElement(attachments, 'attachment',
@ -319,9 +321,18 @@ class VolumeController(wsgi.Controller):
else:
kwargs['snapshot'] = None
source_volid = volume.get('source_volid')
if source_volid is not None:
kwargs['source_volume'] = self.volume_api.get_volume(context,
source_volid)
else:
kwargs['source_volume'] = None
size = volume.get('size', None)
if size is None and kwargs['snapshot'] is not None:
size = kwargs['snapshot']['volume_size']
elif size is None and kwargs['source_volume'] is not None:
size = kwargs['source_volume']['size']
LOG.audit(_("Create volume of %s GB"), size, context=context)

View File

@ -64,6 +64,7 @@ class ViewBuilder(common.ViewBuilder):
'display_description': volume.get('display_description'),
'volume_type': self._get_volume_type(volume),
'snapshot_id': volume.get('snapshot_id'),
'source_volid': volume.get('source_volid'),
'metadata': self._get_volume_metadata(volume),
'links': self._get_links(request, volume['id'])
}

View File

@ -54,6 +54,7 @@ def make_volume(elem):
elem.set('display_description')
elem.set('volume_type')
elem.set('snapshot_id')
elem.set('source_volid')
attachments = xmlutil.SubTemplateElement(elem, 'attachments')
attachment = xmlutil.SubTemplateElement(attachments, 'attachment',
@ -241,9 +242,18 @@ class VolumeController(wsgi.Controller):
else:
kwargs['snapshot'] = None
source_volid = volume.get('source_volid')
if source_volid is not None:
kwargs['source_volume'] = self.volume_api.get_volume(context,
source_volid)
else:
kwargs['source_volume'] = None
size = volume.get('size', None)
if size is None and kwargs['snapshot'] is not None:
size = kwargs['snapshot']['volume_size']
elif size is None and kwargs['source_volume'] is not None:
size = kwargs['source_volume']['size']
LOG.audit(_("Create volume of %s GB"), size, context=context)

View File

@ -0,0 +1,41 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cinder.openstack.common import log as logging
from sqlalchemy import Column
from sqlalchemy import MetaData, String, Table
LOG = logging.getLogger(__name__)
def upgrade(migrate_engine):
"""Add source volume id column to volumes."""
meta = MetaData()
meta.bind = migrate_engine
volumes = Table('volumes', meta, autoload=True)
source_volid = Column('source_volid', String(36))
volumes.create_column(source_volid)
volumes.update().values(source_volid=None).execute()
def downgrade(migrate_engine):
"""Remove source volume id column to volumes."""
meta = MetaData()
meta.bind = migrate_engine
volumes = Table('volumes', meta, autoload=True)
source_volid = Column('source_volid', String(36))
volumes.drop_column(source_volid)

View File

@ -156,6 +156,7 @@ class Volume(BASE, CinderBase):
provider_auth = Column(String(255))
volume_type_id = Column(String(36))
source_volid = Column(String(36))
class VolumeMetadata(BASE, CinderBase):

View File

@ -41,6 +41,7 @@ def stub_volume(id, **kwargs):
'display_description': 'displaydesc',
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'snapshot_id': None,
'source_volid': None,
'volume_type_id': '3e196c20-3c06-11e2-81c1-0800200c9a66',
'volume_metadata': [],
'volume_type': {'name': 'vol_type_name'}}
@ -55,6 +56,7 @@ def stub_volume_create(self, context, size, name, description, snapshot,
vol['size'] = size
vol['display_name'] = name
vol['display_description'] = description
vol['source_volid'] = None
try:
vol['snapshot_id'] = snapshot['id']
except (KeyError, TypeError):

View File

@ -85,6 +85,7 @@ class VolumeApiTest(test.TestCase):
'bootable': 'false',
'volume_type': 'vol_type_name',
'snapshot_id': None,
'source_volid': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1, 1, 1,
@ -143,6 +144,7 @@ class VolumeApiTest(test.TestCase):
'volume_type': 'vol_type_name',
'image_id': test_id,
'snapshot_id': None,
'source_volid': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1, 1, 1,
@ -162,6 +164,7 @@ class VolumeApiTest(test.TestCase):
"display_description": "Volume Test Desc",
"availability_zone": "cinder",
"imageRef": 'c905cedb-7281-47e4-8a62-f26bc5fc4c77',
"source_volid": None,
"snapshot_id": TEST_SNAPSHOT_UUID}
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v1/volumes')
@ -222,6 +225,7 @@ class VolumeApiTest(test.TestCase):
'bootable': 'false',
'volume_type': 'vol_type_name',
'snapshot_id': None,
'source_volid': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
@ -251,6 +255,7 @@ class VolumeApiTest(test.TestCase):
'bootable': 'false',
'volume_type': 'vol_type_name',
'snapshot_id': None,
'source_volid': None,
'metadata': {"qos_max_iops": 2000},
'id': '1',
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
@ -300,6 +305,7 @@ class VolumeApiTest(test.TestCase):
'bootable': 'false',
'volume_type': 'vol_type_name',
'snapshot_id': None,
'source_volid': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1, 1, 1,
@ -323,6 +329,7 @@ class VolumeApiTest(test.TestCase):
'bootable': 'false',
'volume_type': 'vol_type_name',
'snapshot_id': None,
'source_volid': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1, 1, 1,
@ -405,6 +412,7 @@ class VolumeApiTest(test.TestCase):
'bootable': 'false',
'volume_type': 'vol_type_name',
'snapshot_id': None,
'source_volid': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1, 1, 1,
@ -428,6 +436,7 @@ class VolumeApiTest(test.TestCase):
'bootable': 'false',
'volume_type': 'vol_type_name',
'snapshot_id': None,
'source_volid': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1, 1, 1,
@ -455,6 +464,7 @@ class VolumeApiTest(test.TestCase):
'bootable': 'true',
'volume_type': 'vol_type_name',
'snapshot_id': None,
'source_volid': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1, 1, 1,
@ -558,6 +568,7 @@ class VolumeSerializerTest(test.TestCase):
display_description='vol_desc',
volume_type='vol_type',
snapshot_id='snap_id',
source_volid='source_volid',
metadata=dict(foo='bar',
baz='quux', ), )
text = serializer.serialize(dict(volume=raw_volume))
@ -582,6 +593,7 @@ class VolumeSerializerTest(test.TestCase):
display_description='vol1_desc',
volume_type='vol1_type',
snapshot_id='snap1_id',
source_volid=None,
metadata=dict(foo='vol1_foo',
bar='vol1_bar', ), ),
dict(id='vol2_id',
@ -597,6 +609,7 @@ class VolumeSerializerTest(test.TestCase):
display_description='vol2_desc',
volume_type='vol2_type',
snapshot_id='snap2_id',
source_volid=None,
metadata=dict(foo='vol2_foo',
bar='vol2_bar', ), )]
text = serializer.serialize(dict(volumes=raw_volumes))

View File

@ -41,6 +41,7 @@ def stub_volume(id, **kwargs):
'display_description': 'displaydesc',
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'snapshot_id': None,
'source_volid': None,
'volume_type_id': '3e196c20-3c06-11e2-81c1-0800200c9a66',
'volume_metadata': [],
'volume_type': {'name': 'vol_type_name'}}
@ -55,6 +56,7 @@ def stub_volume_create(self, context, size, name, description, snapshot,
vol['size'] = size
vol['display_name'] = name
vol['display_description'] = description
vol['source_volid'] = None
try:
vol['snapshot_id'] = snapshot['id']
except (KeyError, TypeError):

View File

@ -243,6 +243,7 @@ class VolumeApiTest(test.TestCase):
],
'volume_type': 'vol_type_name',
'snapshot_id': None,
'source_volid': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
@ -282,6 +283,7 @@ class VolumeApiTest(test.TestCase):
}],
'volume_type': 'vol_type_name',
'snapshot_id': None,
'source_volid': None,
'metadata': {"qos_max_iops": 2000},
'id': '1',
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
@ -373,6 +375,7 @@ class VolumeApiTest(test.TestCase):
],
'volume_type': 'vol_type_name',
'snapshot_id': None,
'source_volid': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
@ -408,7 +411,6 @@ class VolumeApiTest(test.TestCase):
self.assertEqual(len(resp['volumes']), 3)
# filter on name
req = fakes.HTTPRequest.blank('/v2/volumes?name=vol2')
#import pdb; pdb.set_trace()
resp = self.controller.index(req)
self.assertEqual(len(resp['volumes']), 1)
self.assertEqual(resp['volumes'][0]['name'], 'vol2')
@ -473,6 +475,7 @@ class VolumeApiTest(test.TestCase):
],
'volume_type': 'vol_type_name',
'snapshot_id': None,
'source_volid': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
@ -508,6 +511,7 @@ class VolumeApiTest(test.TestCase):
'attachments': [],
'volume_type': 'vol_type_name',
'snapshot_id': None,
'source_volid': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
@ -584,7 +588,7 @@ class VolumeSerializerTest(test.TestCase):
for attr in ('id', 'status', 'size', 'availability_zone', 'created_at',
'name', 'display_description', 'volume_type',
'snapshot_id'):
'snapshot_id', 'source_volid'):
self.assertEqual(str(vol[attr]), tree.get(attr))
for child in tree:
@ -623,6 +627,7 @@ class VolumeSerializerTest(test.TestCase):
display_description='vol_desc',
volume_type='vol_type',
snapshot_id='snap_id',
source_volid='source_volid',
metadata=dict(
foo='bar',
baz='quux',
@ -656,6 +661,7 @@ class VolumeSerializerTest(test.TestCase):
display_description='vol1_desc',
volume_type='vol1_type',
snapshot_id='snap1_id',
source_volid=None,
metadata=dict(foo='vol1_foo',
bar='vol1_bar', ), ),
dict(
@ -672,6 +678,7 @@ class VolumeSerializerTest(test.TestCase):
display_description='vol2_desc',
volume_type='vol2_type',
snapshot_id='snap2_id',
source_volid=None,
metadata=dict(foo='vol2_foo',
bar='vol2_bar', ), )]
text = serializer.serialize(dict(volumes=raw_volumes))

View File

@ -333,3 +333,20 @@ class TestMigrations(test.TestCase):
sqlalchemy.types.VARCHAR))
self.assertTrue(extra_specs.c.volume_type_id.foreign_keys)
def test_migration_005(self):
"""Test that adding source_volid column works correctly."""
for (key, engine) in self.engines.items():
migration_api.version_control(engine,
TestMigrations.REPOSITORY,
migration.INIT_VERSION)
migration_api.upgrade(engine, TestMigrations.REPOSITORY, 4)
metadata = sqlalchemy.schema.MetaData()
metadata.bind = engine
migration_api.upgrade(engine, TestMigrations.REPOSITORY, 5)
volumes = sqlalchemy.Table('volumes',
metadata,
autoload=True)
self.assertTrue(isinstance(volumes.c.source_volid.type,
sqlalchemy.types.VARCHAR))

View File

@ -113,7 +113,9 @@ class VolumeRpcAPITestCase(test.TestCase):
volume=self.fake_volume,
host='fake_host1',
snapshot_id='fake_snapshot_id',
image_id='fake_image_id')
image_id='fake_image_id',
source_volid='fake_src_id',
version='1.1')
def test_delete_volume(self):
self._test_volume_api('delete_volume',

View File

@ -87,7 +87,13 @@ class API(base.Base):
def create(self, context, size, name, description, snapshot=None,
image_id=None, volume_type=None, metadata=None,
availability_zone=None):
availability_zone=None, source_volume=None):
if ((snapshot is not None) and (source_volume is not None)):
msg = (_("May specify either snapshot, "
"or src volume but not both!"))
raise exception.InvalidInput(reason=msg)
check_policy(context, 'create')
if snapshot is not None:
if snapshot['status'] != "available":
@ -100,6 +106,21 @@ class API(base.Base):
else:
snapshot_id = None
if source_volume is not None:
if source_volume['status'] == "error":
msg = _("Unable to clone volumes that are in an error state")
raise exception.InvalidSourceVolume(reason=msg)
if not size:
size = source_volume['size']
else:
if size < source_volume['size']:
msg = _("Clones currently must be "
">= original volume size.")
raise exception.InvalidInput(reason=msg)
source_volid = source_volume['id']
else:
source_volid = None
def as_int(s):
try:
return int(s)
@ -114,7 +135,7 @@ class API(base.Base):
% size)
raise exception.InvalidInput(reason=msg)
if image_id:
if (image_id and not (source_volume or snapshot)):
# check image existence
image_meta = self.image_service.show(context, image_id)
image_size_in_gb = (int(image_meta['size']) + GB - 1) / GB
@ -151,10 +172,13 @@ class API(base.Base):
if availability_zone is None:
availability_zone = FLAGS.storage_availability_zone
if not volume_type:
if not volume_type and not source_volume:
volume_type = volume_types.get_default_volume_type()
volume_type_id = volume_type.get('id')
if not volume_type and source_volume:
volume_type_id = source_volume['volume_type_id']
else:
volume_type_id = volume_type.get('id')
options = {'size': size,
'user_id': context.user_id,
@ -166,7 +190,8 @@ class API(base.Base):
'display_name': name,
'display_description': description,
'volume_type_id': volume_type_id,
'metadata': metadata, }
'metadata': metadata,
'source_volid': source_volid}
try:
volume = self.db.volume_create(context, options)
@ -182,7 +207,8 @@ class API(base.Base):
'volume_type': volume_type,
'volume_id': volume['id'],
'snapshot_id': volume['snapshot_id'],
'image_id': image_id}
'image_id': image_id,
'source_volid': volume['source_volid']}
filter_properties = {}
@ -196,16 +222,18 @@ class API(base.Base):
# If snapshot_id is set, make the call create volume directly to
# the volume host where the snapshot resides instead of passing it
# through the scheduler. So snapshot can be copy to new volume.
source_volid = request_spec['source_volid']
volume_id = request_spec['volume_id']
snapshot_id = request_spec['snapshot_id']
image_id = request_spec['image_id']
if snapshot_id and FLAGS.snapshot_same_host:
snapshot_ref = self.db.snapshot_get(context, snapshot_id)
src_volume_ref = self.db.volume_get(context,
snapshot_ref['volume_id'])
source_volume_ref = self.db.volume_get(context,
snapshot_ref['volume_id'])
now = timeutils.utcnow()
values = {'host': src_volume_ref['host'], 'scheduled_at': now}
values = {'host': source_volume_ref['host'], 'scheduled_at': now}
volume_ref = self.db.volume_update(context, volume_id, values)
# bypass scheduler and send request directly to volume
@ -214,6 +242,20 @@ class API(base.Base):
volume_ref['host'],
snapshot_id,
image_id)
elif source_volid:
source_volume_ref = self.db.volume_get(context,
source_volid)
now = timeutils.utcnow()
values = {'host': source_volume_ref['host'], 'scheduled_at': now}
volume_ref = self.db.volume_update(context, volume_id, values)
# bypass scheduler and send request directly to volume
self.volume_rpcapi.create_volume(context,
volume_ref,
volume_ref['host'],
snapshot_id,
image_id,
source_volid)
else:
self.scheduler_rpcapi.create_volume(
context,
@ -319,6 +361,11 @@ class API(base.Base):
rv = self.db.snapshot_get(context, snapshot_id)
return dict(rv.iteritems())
def get_volume(self, context, volume_id):
check_policy(context, 'get_volume')
rv = self.db.volume_get(context, volume_id)
return dict(rv.iteritems())
def get_all_snapshots(self, context, search_opts=None):
check_policy(context, 'get_all_snapshots')

View File

@ -149,6 +149,8 @@ class VolumeDriver(object):
# TODO(ja): reclaiming space should be done lazy and low priority
dev_path = self.local_path(volume)
if FLAGS.secure_delete and os.path.exists(dev_path):
LOG.info(_("Performing secure delete on volume: %s")
% volume['id'])
self._copy_volume('/dev/zero', dev_path, size_in_g)
self._try_execute('lvremove', '-f', "%s/%s" %
@ -179,6 +181,23 @@ class VolumeDriver(object):
self._copy_volume(self.local_path(snapshot), self.local_path(volume),
snapshot['volume_size'])
def create_cloned_volume(self, volume, src_vref):
"""Creates a clone of the specified volume."""
LOG.info(_('Creating clone of volume: %s') % src_vref['id'])
volume_name = FLAGS.volume_name_template % src_vref['id']
temp_snapshot = {'volume_name': volume_name,
'size': src_vref['size'],
'volume_size': src_vref['size'],
'name': 'clone-snap-%s' % src_vref['id']}
self.create_snapshot(temp_snapshot)
self._create_volume(volume['name'], self._sizestr(volume['size']))
try:
self._copy_volume(self.local_path(temp_snapshot),
self.local_path(volume),
src_vref['size'])
finally:
self.delete_snapshot(temp_snapshot)
def delete_volume(self, volume):
"""Deletes a logical volume."""
if self._volume_not_present(volume['name']):

View File

@ -997,6 +997,10 @@ class NetAppISCSIDriver(driver.ISCSIDriver):
self._refresh_dfm_luns(lun.HostId)
self._discover_dataset_luns(dataset, clone_name)
def create_cloned_volume(self, volume, src_vref):
"""Creates a clone of the specified volume."""
raise NotImplementedError()
class NetAppLun(object):
"""Represents a LUN on NetApp storage."""
@ -1306,3 +1310,7 @@ class NetAppCmodeISCSIDriver(driver.ISCSIDriver):
def copy_volume_to_image(self, context, volume, image_service, image_id):
"""Copy the volume to the specified image."""
raise NotImplementedError()
def create_cloned_volume(self, volume, src_vref):
"""Creates a clone of the specified volume."""
raise NotImplementedError()

View File

@ -287,3 +287,7 @@ class NexentaDriver(driver.ISCSIDriver): # pylint: disable=R0921
def copy_volume_to_image(self, context, volume, image_service, image_id):
"""Copy the volume to the specified image."""
raise NotImplementedError()
def create_cloned_volume(self, volume, src_vref):
"""Creates a clone of the specified volume."""
raise NotImplementedError()

View File

@ -75,6 +75,9 @@ class NfsDriver(driver.VolumeDriver):
"""Just to override parent behavior"""
pass
def create_cloned_volume(self, volume, src_vref):
raise NotImplementedError()
def create_volume(self, volume):
"""Creates a volume"""

View File

@ -64,6 +64,9 @@ class RBDDriver(driver.VolumeDriver):
stdout, _ = self._execute('rbd', '--help')
return 'clone' in stdout
def create_cloned_volume(self, volume, src_vref):
raise NotImplementedError()
def create_volume(self, volume):
"""Creates a logical volume."""
if int(volume['size']) == 0:

View File

@ -161,3 +161,7 @@ class SanISCSIDriver(ISCSIDriver):
def copy_volume_to_image(self, context, volume, image_service, image_id):
"""Copy the volume to the specified image."""
raise NotImplementedError()
def create_cloned_volume(self, volume, src_vref):
"""Create a cloen of the specified volume."""
raise NotImplementedError()

View File

@ -46,6 +46,9 @@ class SheepdogDriver(driver.VolumeDriver):
exception_message = _("Sheepdog is not working")
raise exception.VolumeBackendAPIException(data=exception_message)
def create_cloned_volume(self, volume, src_vref):
raise NotImplementedError()
def create_volume(self, volume):
"""Creates a sheepdog volume"""
self._try_execute('qemu-img', 'create',

View File

@ -58,6 +58,9 @@ class XenAPINFSDriver(driver.VolumeDriver):
)
self.nfs_ops = xenapi_lib.NFSBasedVolumeOperations(session_factory)
def create_cloned_volume(self, volume, src_vref):
raise NotImplementedError()
def create_volume(self, volume):
volume_details = self.nfs_ops.create_volume(
FLAGS.xenapi_nfs_server,

View File

@ -486,3 +486,7 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
def copy_volume_to_image(self, context, volume, image_service, image_id):
"""Copy the volume to the specified image."""
raise NotImplementedError()
def create_cloned_volume(self, volume, src_vref):
"""Creates a clone of the specified volume."""
raise NotImplementedError()

View File

@ -102,7 +102,7 @@ MAPPING = {
class VolumeManager(manager.SchedulerDependentManager):
"""Manages attachable block storage devices."""
RPC_API_VERSION = '1.0'
RPC_API_VERSION = '1.1'
def __init__(self, volume_driver=None, *args, **kwargs):
"""Load the driver from the one specified in args, or from flags."""
@ -144,7 +144,7 @@ class VolumeManager(manager.SchedulerDependentManager):
self.delete_volume(ctxt, volume['id'])
def create_volume(self, context, volume_id, snapshot_id=None,
image_id=None):
image_id=None, source_volid=None):
"""Creates and exports the volume."""
context = context.elevated()
volume_ref = self.db.volume_get(context, volume_id)
@ -164,13 +164,17 @@ class VolumeManager(manager.SchedulerDependentManager):
vol_size = volume_ref['size']
LOG.debug(_("volume %(vol_name)s: creating lv of"
" size %(vol_size)sG") % locals())
if snapshot_id is None and image_id is None:
if all(x is None for x in(snapshot_id, image_id, source_volid)):
model_update = self.driver.create_volume(volume_ref)
elif snapshot_id is not None:
snapshot_ref = self.db.snapshot_get(context, snapshot_id)
model_update = self.driver.create_volume_from_snapshot(
volume_ref,
snapshot_ref)
elif source_volid is not None:
src_vref = self.db.volume_get(context, source_volid)
model_update = self.driver.create_cloned_volume(volume_ref,
src_vref)
else:
# create the volume from an image
image_service, image_id = \
@ -375,7 +379,7 @@ class VolumeManager(manager.SchedulerDependentManager):
# Check for https://bugs.launchpad.net/cinder/+bug/1065702
volume_ref = self.db.volume_get(context, volume_id)
if (volume_ref['provider_location'] and
volume_ref['name'] not in volume_ref['provider_location']):
volume_ref['name'] not in volume_ref['provider_location']):
self.driver.ensure_export(context, volume_ref)
def _copy_image_to_volume(self, context, volume, image_id):

View File

@ -33,6 +33,7 @@ class VolumeAPI(cinder.openstack.common.rpc.proxy.RpcProxy):
API version history:
1.0 - Initial version.
1.1 - Adds clone volume option to create_volume.
'''
BASE_RPC_API_VERSION = '1.0'
@ -43,15 +44,18 @@ class VolumeAPI(cinder.openstack.common.rpc.proxy.RpcProxy):
default_version=self.BASE_RPC_API_VERSION)
def create_volume(self, ctxt, volume, host,
snapshot_id=None, image_id=None):
snapshot_id=None, image_id=None,
source_volid=None):
self.cast(ctxt,
self.make_msg('create_volume',
volume_id=volume['id'],
snapshot_id=snapshot_id,
image_id=image_id),
image_id=image_id,
source_volid=source_volid),
topic=rpc.queue_get_for(ctxt,
self.topic,
host))
host),
version='1.1')
def delete_volume(self, ctxt, volume):
self.cast(ctxt,