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:
parent
18269eaad8
commit
d99fb6011c
@ -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)
|
||||
|
||||
|
@ -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'])
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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))
|
||||
|
@ -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):
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
|
@ -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',
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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']):
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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"""
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user