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
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',
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)
'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'])
}
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',
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)
--- /dev/null
+# 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)
provider_auth = Column(String(255))
volume_type_id = Column(String(36))
+ source_volid = Column(String(36))
class VolumeMetadata(BASE, CinderBase):
'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'}}
vol['size'] = size
vol['display_name'] = name
vol['display_description'] = description
+ vol['source_volid'] = None
try:
vol['snapshot_id'] = snapshot['id']
except (KeyError, TypeError):
'bootable': 'false',
'volume_type': 'vol_type_name',
'snapshot_id': None,
+ 'source_volid': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1, 1, 1,
'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,
"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')
'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),
'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),
'bootable': 'false',
'volume_type': 'vol_type_name',
'snapshot_id': None,
+ 'source_volid': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1, 1, 1,
'bootable': 'false',
'volume_type': 'vol_type_name',
'snapshot_id': None,
+ 'source_volid': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1, 1, 1,
'bootable': 'false',
'volume_type': 'vol_type_name',
'snapshot_id': None,
+ 'source_volid': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1, 1, 1,
'bootable': 'false',
'volume_type': 'vol_type_name',
'snapshot_id': None,
+ 'source_volid': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1, 1, 1,
'bootable': 'true',
'volume_type': 'vol_type_name',
'snapshot_id': None,
+ 'source_volid': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1, 1, 1,
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))
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',
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))
'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'}}
vol['size'] = size
vol['display_name'] = name
vol['display_description'] = description
+ vol['source_volid'] = None
try:
vol['snapshot_id'] = snapshot['id']
except (KeyError, TypeError):
],
'volume_type': 'vol_type_name',
'snapshot_id': None,
+ 'source_volid': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
}],
'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),
],
'volume_type': 'vol_type_name',
'snapshot_id': None,
+ 'source_volid': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
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')
],
'volume_type': 'vol_type_name',
'snapshot_id': None,
+ 'source_volid': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'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),
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:
display_description='vol_desc',
volume_type='vol_type',
snapshot_id='snap_id',
+ source_volid='source_volid',
metadata=dict(
foo='bar',
baz='quux',
display_description='vol1_desc',
volume_type='vol1_type',
snapshot_id='snap1_id',
+ source_volid=None,
metadata=dict(foo='vol1_foo',
bar='vol1_bar', ), ),
dict(
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))
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))
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',
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":
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)
% 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
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,
'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)
'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 = {}
# 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
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,
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')
# 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" %
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']):
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."""
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()
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()
"""Just to override parent behavior"""
pass
+ def create_cloned_volume(self, volume, src_vref):
+ raise NotImplementedError()
+
def create_volume(self, volume):
"""Creates a volume"""
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:
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()
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',
)
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,
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()
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."""
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)
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 = \
# 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):
API version history:
1.0 - Initial version.
+ 1.1 - Adds clone volume option to create_volume.
'''
BASE_RPC_API_VERSION = '1.0'
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,