]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Implement ability to Clone volumes in Cinder.
authorJohn Griffith <john.griffith@solidfire.com>
Wed, 12 Dec 2012 22:23:56 +0000 (15:23 -0700)
committerJohn Griffith <john.griffith@solidfire.com>
Wed, 2 Jan 2013 15:55:12 +0000 (08:55 -0700)
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

23 files changed:
cinder/api/v1/volumes.py
cinder/api/v2/views/volumes.py
cinder/api/v2/volumes.py
cinder/db/sqlalchemy/migrate_repo/versions/005_add_source_volume_column.py [new file with mode: 0644]
cinder/db/sqlalchemy/models.py
cinder/tests/api/v1/stubs.py
cinder/tests/api/v1/test_volumes.py
cinder/tests/api/v2/stubs.py
cinder/tests/api/v2/test_volumes.py
cinder/tests/test_migrations.py
cinder/tests/test_volume_rpcapi.py
cinder/volume/api.py
cinder/volume/driver.py
cinder/volume/drivers/netapp.py
cinder/volume/drivers/nexenta/volume.py
cinder/volume/drivers/nfs.py
cinder/volume/drivers/rbd.py
cinder/volume/drivers/san/san.py
cinder/volume/drivers/sheepdog.py
cinder/volume/drivers/xenapi/sm.py
cinder/volume/drivers/zadara.py
cinder/volume/manager.py
cinder/volume/rpcapi.py

index acf824fa3aed7e70baf9c2a0480ebf92d21ac0e4..b04b2da9ec05d0cbfaa2c5a06e49c67de30c4f32 100644 (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)
 
index a30ec03858e05b6e399a9ea6e96137f9d2a4fb4e..e34623e0254aa6190b4c961bb439f47cc8e44236 100644 (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'])
             }
index cd5ae6963c32c2d89e100cdc6403e6fde108959a..8f412d7d3dd398640d285be11f17d00d39977466 100644 (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)
 
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 (file)
index 0000000..d20cda9
--- /dev/null
@@ -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)
index 542d0358603877b827038541da496b51cbb7d2b0..9389a405428e09604220d3764038512b5cc3f9d9 100644 (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):
index 2d8d1403268fac10bf16da50d6e0d558481e47f4..72d91dbf1b83c6b427bce546bc6a60c8289f1c85 100644 (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):
index fd5e7f91340677d6cdb65c35a5686d4dbc39b4f3..e1663c71cc740efc9fad8873667b4ef9f0e8ca89 100644 (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))
index 2d8d1403268fac10bf16da50d6e0d558481e47f4..72d91dbf1b83c6b427bce546bc6a60c8289f1c85 100644 (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):
index 0e22fcb4491373ada623fa612a3008563214355d..8428f16f647447e3be0ef7040801dee0fe7b3853 100644 (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))
index f6eaf6c6eef54ba51005b47aaef65564d1e18a5e..b669fb5c46e7cf2ba1ef611ac7fb6ef4f0a608cf 100644 (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))
index 9126757e6f55acfbc87d023e6fcfa669d5f6ec0b..0b785387daf854a29ace1443bd10dddb152f51dc 100644 (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',
index 5c95233689d4e5aef7749f7ec17c158278a4f7e9..21d1274d93c73814e8a80319d621a82efecf793a 100644 (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')
 
index 2dac38f59f0312b84f702a36312e02cdc2861bc1..6626ddf701346372a9901d68667e09e95cdf24d8 100644 (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']):
index e759e0d42893d52acc2ed79fe094ccc809860bfc..3ce77c5c2191580d6cda6abf4bc50345221f7160 100644 (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()
index 6769f301ae2073e92f9fd2b45e3fa56b2fa5df77..cdff190cbfde0458b15b7a9cac02c0d2f4ec89ad 100644 (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()
index 4004a4c80ad316c60908f562e280c51c23377f27..e47c5714f776dae5ae7740b98d96d482370d96f7 100644 (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"""
 
index e98a1de36b666aa1f724d422d668445c1b1e8a05..cc2fb53ba5c3da55f6347c4e6e4ecfa7d7934669 100644 (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:
index 57299d8c95eaf7efd5586936466690cb3788681d..ec5f65195f57c02dd609ae2aa08e96fa2c74fae5 100644 (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()
index 3b615b6e78058a3f637ecbb7ccd3af2f77404ab8..ed1466ddbeebdd2d5e4800d837aca3980a0a0810 100644 (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',
index 8e767787bd2f90f13bdb913e25b3ff45ea1ef09f..f1f7936752578d60c61e3a71abcd2b1dc6d4a38f 100644 (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,
index dee160742b7aa73bf890743772d89ce1889d9417..912b67117767a99a6208b798d337634cd38824c3 100644 (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()
index 1244f6f4be9de6086186f2791b6300c48fbac911..e341f2fd8f34855f26645f9cdd915053464f4e46 100644 (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):
index 47a64392aca503841f5563f4b2016e355e297280..54bfabd03c391f74bcecb90280b398876f470f63 100644 (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,