From: John Griffith Date: Wed, 12 Dec 2012 22:23:56 +0000 (-0700) Subject: Implement ability to Clone volumes in Cinder. X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=d99fb6011cc610b78d42891800189feed0391742;p=openstack-build%2Fcinder-build.git 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 --- diff --git a/cinder/api/v1/volumes.py b/cinder/api/v1/volumes.py index acf824fa3..b04b2da9e 100644 --- a/cinder/api/v1/volumes.py +++ b/cinder/api/v1/volumes.py @@ -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) diff --git a/cinder/api/v2/views/volumes.py b/cinder/api/v2/views/volumes.py index a30ec0385..e34623e02 100644 --- a/cinder/api/v2/views/volumes.py +++ b/cinder/api/v2/views/volumes.py @@ -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']) } diff --git a/cinder/api/v2/volumes.py b/cinder/api/v2/volumes.py index cd5ae6963..8f412d7d3 100644 --- a/cinder/api/v2/volumes.py +++ b/cinder/api/v2/volumes.py @@ -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) diff --git a/cinder/db/sqlalchemy/migrate_repo/versions/005_add_source_volume_column.py b/cinder/db/sqlalchemy/migrate_repo/versions/005_add_source_volume_column.py new file mode 100644 index 000000000..d20cda953 --- /dev/null +++ b/cinder/db/sqlalchemy/migrate_repo/versions/005_add_source_volume_column.py @@ -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) diff --git a/cinder/db/sqlalchemy/models.py b/cinder/db/sqlalchemy/models.py index 542d03586..9389a4054 100644 --- a/cinder/db/sqlalchemy/models.py +++ b/cinder/db/sqlalchemy/models.py @@ -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): diff --git a/cinder/tests/api/v1/stubs.py b/cinder/tests/api/v1/stubs.py index 2d8d14032..72d91dbf1 100644 --- a/cinder/tests/api/v1/stubs.py +++ b/cinder/tests/api/v1/stubs.py @@ -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): diff --git a/cinder/tests/api/v1/test_volumes.py b/cinder/tests/api/v1/test_volumes.py index fd5e7f913..e1663c71c 100644 --- a/cinder/tests/api/v1/test_volumes.py +++ b/cinder/tests/api/v1/test_volumes.py @@ -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)) diff --git a/cinder/tests/api/v2/stubs.py b/cinder/tests/api/v2/stubs.py index 2d8d14032..72d91dbf1 100644 --- a/cinder/tests/api/v2/stubs.py +++ b/cinder/tests/api/v2/stubs.py @@ -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): diff --git a/cinder/tests/api/v2/test_volumes.py b/cinder/tests/api/v2/test_volumes.py index 0e22fcb44..8428f16f6 100644 --- a/cinder/tests/api/v2/test_volumes.py +++ b/cinder/tests/api/v2/test_volumes.py @@ -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)) diff --git a/cinder/tests/test_migrations.py b/cinder/tests/test_migrations.py index f6eaf6c6e..b669fb5c4 100644 --- a/cinder/tests/test_migrations.py +++ b/cinder/tests/test_migrations.py @@ -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)) diff --git a/cinder/tests/test_volume_rpcapi.py b/cinder/tests/test_volume_rpcapi.py index 9126757e6..0b785387d 100644 --- a/cinder/tests/test_volume_rpcapi.py +++ b/cinder/tests/test_volume_rpcapi.py @@ -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', diff --git a/cinder/volume/api.py b/cinder/volume/api.py index 5c9523368..21d1274d9 100644 --- a/cinder/volume/api.py +++ b/cinder/volume/api.py @@ -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') diff --git a/cinder/volume/driver.py b/cinder/volume/driver.py index 2dac38f59..6626ddf70 100644 --- a/cinder/volume/driver.py +++ b/cinder/volume/driver.py @@ -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']): diff --git a/cinder/volume/drivers/netapp.py b/cinder/volume/drivers/netapp.py index e759e0d42..3ce77c5c2 100644 --- a/cinder/volume/drivers/netapp.py +++ b/cinder/volume/drivers/netapp.py @@ -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() diff --git a/cinder/volume/drivers/nexenta/volume.py b/cinder/volume/drivers/nexenta/volume.py index 6769f301a..cdff190cb 100644 --- a/cinder/volume/drivers/nexenta/volume.py +++ b/cinder/volume/drivers/nexenta/volume.py @@ -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() diff --git a/cinder/volume/drivers/nfs.py b/cinder/volume/drivers/nfs.py index 4004a4c80..e47c5714f 100644 --- a/cinder/volume/drivers/nfs.py +++ b/cinder/volume/drivers/nfs.py @@ -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""" diff --git a/cinder/volume/drivers/rbd.py b/cinder/volume/drivers/rbd.py index e98a1de36..cc2fb53ba 100644 --- a/cinder/volume/drivers/rbd.py +++ b/cinder/volume/drivers/rbd.py @@ -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: diff --git a/cinder/volume/drivers/san/san.py b/cinder/volume/drivers/san/san.py index 57299d8c9..ec5f65195 100644 --- a/cinder/volume/drivers/san/san.py +++ b/cinder/volume/drivers/san/san.py @@ -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() diff --git a/cinder/volume/drivers/sheepdog.py b/cinder/volume/drivers/sheepdog.py index 3b615b6e7..ed1466ddb 100644 --- a/cinder/volume/drivers/sheepdog.py +++ b/cinder/volume/drivers/sheepdog.py @@ -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', diff --git a/cinder/volume/drivers/xenapi/sm.py b/cinder/volume/drivers/xenapi/sm.py index 8e767787b..f1f793675 100644 --- a/cinder/volume/drivers/xenapi/sm.py +++ b/cinder/volume/drivers/xenapi/sm.py @@ -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, diff --git a/cinder/volume/drivers/zadara.py b/cinder/volume/drivers/zadara.py index dee160742..912b67117 100644 --- a/cinder/volume/drivers/zadara.py +++ b/cinder/volume/drivers/zadara.py @@ -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() diff --git a/cinder/volume/manager.py b/cinder/volume/manager.py index 1244f6f4b..e341f2fd8 100644 --- a/cinder/volume/manager.py +++ b/cinder/volume/manager.py @@ -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): diff --git a/cinder/volume/rpcapi.py b/cinder/volume/rpcapi.py index 47a64392a..54bfabd03 100644 --- a/cinder/volume/rpcapi.py +++ b/cinder/volume/rpcapi.py @@ -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,