add ability to clone images

Given the backend location from Glance, drivers can determine
whether they can clone or otherwise efficiently create a volume
from the image without downloading all the data from Glance.

For now implement cloning for the RBD driver. There's already a
Glance backend that stores images as RBD snapshots, so they're
ready to be cloned into volumes. Fall back to copying all the
data if cloning is not possible.

Change-Id: I71a8172bd22a5bbf64d4c68631630125fcc7fd34
Implements: blueprint efficient-volumes-from-images
Signed-off-by: Josh Durgin <josh.durgin@inktank.com>
This commit is contained in:
Josh Durgin 2012-08-14 12:27:48 -07:00
parent 87ba5de73e
commit edc11101cb
4 changed files with 261 additions and 8 deletions

161
cinder/tests/test_rbd.py Normal file
View File

@ -0,0 +1,161 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 Josh Durgin
# All Rights Reserved.
#
# 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 import db
from cinder import exception
from cinder.openstack.common import log as logging
from cinder.openstack.common import timeutils
from cinder import test
from cinder.tests.image import fake as fake_image
from cinder.tests.test_volume import DriverTestCase
from cinder.volume.driver import RBDDriver
LOG = logging.getLogger(__name__)
class RBDTestCase(test.TestCase):
def setUp(self):
super(RBDTestCase, self).setUp()
def fake_execute(*args):
pass
self.driver = RBDDriver(execute=fake_execute)
def test_good_locations(self):
locations = [
'rbd://fsid/pool/image/snap',
'rbd://%2F/%2F/%2F/%2F',
]
map(self.driver._parse_location, locations)
def test_bad_locations(self):
locations = [
'rbd://image',
'http://path/to/somewhere/else',
'rbd://image/extra',
'rbd://image/',
'rbd://fsid/pool/image/',
'rbd://fsid/pool/image/snap/',
'rbd://///',
]
for loc in locations:
self.assertRaises(exception.ImageUnacceptable,
self.driver._parse_location,
loc)
self.assertFalse(self.driver._is_cloneable(loc))
def test_cloneable(self):
self.stubs.Set(self.driver, '_get_fsid', lambda: 'abc')
location = 'rbd://abc/pool/image/snap'
self.assertTrue(self.driver._is_cloneable(location))
def test_uncloneable_different_fsid(self):
self.stubs.Set(self.driver, '_get_fsid', lambda: 'abc')
location = 'rbd://def/pool/image/snap'
self.assertFalse(self.driver._is_cloneable(location))
def test_uncloneable_unreadable(self):
def fake_exc(*args):
raise exception.ProcessExecutionError()
self.stubs.Set(self.driver, '_get_fsid', lambda: 'abc')
self.stubs.Set(self.driver, '_execute', fake_exc)
location = 'rbd://abc/pool/image/snap'
self.assertFalse(self.driver._is_cloneable(location))
class FakeRBDDriver(RBDDriver):
def _clone(self):
pass
def _resize(self):
pass
class ManagedRBDTestCase(DriverTestCase):
driver_name = "cinder.tests.test_rbd.FakeRBDDriver"
def setUp(self):
super(ManagedRBDTestCase, self).setUp()
fake_image.stub_out_image_service(self.stubs)
def _clone_volume_from_image(self, expected_status,
clone_works=True):
"""Try to clone a volume from an image, and check the status
afterwards"""
def fake_clone_image(volume, image_location):
pass
def fake_clone_error(volume, image_location):
raise exception.CinderException()
self.stubs.Set(self.volume.driver, '_is_cloneable', lambda x: True)
if clone_works:
self.stubs.Set(self.volume.driver, 'clone_image', fake_clone_image)
else:
self.stubs.Set(self.volume.driver, 'clone_image', fake_clone_error)
image_id = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
volume_id = 1
# creating volume testdata
db.volume_create(self.context, {'id': volume_id,
'updated_at': timeutils.utcnow(),
'display_description': 'Test Desc',
'size': 20,
'status': 'creating',
'instance_uuid': None,
'host': 'dummy'})
try:
if clone_works:
self.volume.create_volume(self.context,
volume_id,
image_id=image_id)
else:
self.assertRaises(exception.CinderException,
self.volume.create_volume,
self.context,
volume_id,
image_id=image_id)
volume = db.volume_get(self.context, volume_id)
self.assertEqual(volume['status'], expected_status)
finally:
# cleanup
db.volume_destroy(self.context, volume_id)
def test_clone_image_status_available(self):
"""Verify that before cloning, an image is in the available state."""
self._clone_volume_from_image('available', True)
def test_clone_image_status_error(self):
"""Verify that before cloning, an image is in the available state."""
self._clone_volume_from_image('error', False)
def test_clone_success(self):
self.stubs.Set(self.volume.driver, '_is_cloneable', lambda x: True)
self.stubs.Set(self.volume.driver, 'clone_image', lambda a, b: True)
image_id = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
self.assertTrue(self.volume.driver.clone_image({}, image_id))
def test_clone_bad_image_id(self):
self.stubs.Set(self.volume.driver, '_is_cloneable', lambda x: True)
self.assertFalse(self.volume.driver.clone_image({}, None))
def test_clone_uncloneable(self):
self.stubs.Set(self.volume.driver, '_is_cloneable', lambda x: False)
self.assertFalse(self.volume.driver.clone_image({}, 'dne'))

View File

@ -116,7 +116,7 @@ class API(base.Base):
if image_id:
# check image existence
image_meta = self.image_service.show(context, image_id)
image_size_in_gb = image_meta['size'] / GB
image_size_in_gb = int(image_meta['size']) / GB
#check image size is not larger than volume size.
if image_size_in_gb > size:
msg = _('Size of specified image is larger than volume size.')

View File

@ -20,7 +20,9 @@ Drivers for volumes.
"""
import tempfile
import time
import urllib
from cinder import exception
from cinder import flags
@ -64,6 +66,10 @@ volume_opts = [
default=None,
help='the libvirt uuid of the secret for the rbd_user'
'volumes'),
cfg.StrOpt('volume_tmp_dir',
default=None,
help='where to store temporary image files if the volume '
'driver does not write them directly to the volume'),
]
FLAGS = flags.FLAGS
@ -245,6 +251,17 @@ class VolumeDriver(object):
"""Copy the volume to the specified image."""
raise NotImplementedError()
def clone_image(self, volume, image_location):
"""Create a volume efficiently from an existing image.
image_location is a string whose format depends on the
image service backend in use. The driver should use it
to determine whether cloning is possible.
Returns a boolean indicating whether cloning occurred
"""
return False
class ISCSIDriver(VolumeDriver):
"""Executes commands relating to ISCSI volumes.
@ -642,6 +659,72 @@ class RBDDriver(VolumeDriver):
def terminate_connection(self, volume, connector):
pass
def _parse_location(self, location):
prefix = 'rbd://'
if not location.startswith(prefix):
reason = _('Image %s is not stored in rbd') % location
raise exception.ImageUnacceptable(reason)
pieces = map(urllib.unquote, location[len(prefix):].split('/'))
if any(map(lambda p: p == '', pieces)):
reason = _('Image %s has blank components') % location
raise exception.ImageUnacceptable(reason)
if len(pieces) != 4:
reason = _('Image %s is not an rbd snapshot') % location
raise exception.ImageUnacceptable(reason)
return pieces
def _get_fsid(self):
stdout, _ = self._execute('ceph', 'fsid')
return stdout.rstrip('\n')
def _is_cloneable(self, image_location):
try:
fsid, pool, image, snapshot = self._parse_location(image_location)
except exception.ImageUnacceptable:
return False
if self._get_fsid() != fsid:
reason = _('%s is in a different ceph cluster') % image_location
LOG.debug(reason)
return False
# check that we can read the image
try:
self._execute('rbd', 'info',
'--pool', pool,
'--image', image,
'--snap', snapshot)
except exception.ProcessExecutionError:
LOG.debug(_('Unable to read image %s') % image_location)
return False
return True
def clone_image(self, volume, image_location):
if image_location is None or not self._is_cloneable(image_location):
return False
_, pool, image, snapshot = self._parse_location(image_location)
self._clone(volume, pool, image, snapshot)
self._resize(volume)
return True
def copy_image_to_volume(self, context, volume, image_service, image_id):
# TODO(jdurgin): replace with librbd
# this is a temporary hack, since rewriting this driver
# to use librbd would take too long
if FLAGS.volume_tmp_dir and not os.exists(FLAGS.volume_tmp_dir):
os.makedirs(FLAGS.volume_tmp_dir)
with tempfile.NamedTemporaryFile(dir=FLAGS.volume_tmp_dir) as tmp:
image_service.download(context, image_id, tmp)
# import creates the image, so we must remove it first
self._try_execute('rbd', 'rm',
'--pool', FLAGS.rbd_pool,
volume['name'])
self._try_execute('rbd', 'import',
'--pool', FLAGS.rbd_pool,
tmp.name, volume['name'])
class SheepdogDriver(VolumeDriver):
"""Executes commands relating to Sheepdog Volumes"""

View File

@ -112,23 +112,32 @@ class VolumeManager(manager.SchedulerDependentManager):
# before passing it to the driver.
volume_ref['host'] = self.host
if image_id:
status = 'downloading'
else:
status = 'available'
status = 'available'
model_update = False
try:
vol_name = volume_ref['name']
vol_size = volume_ref['size']
LOG.debug(_("volume %(vol_name)s: creating lv of"
" size %(vol_size)sG") % locals())
if snapshot_id is None:
if snapshot_id is None and image_id is None:
model_update = self.driver.create_volume(volume_ref)
else:
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)
else:
# create the volume from an image
image_service, image_id = \
glance.get_remote_image_service(context,
image_id)
image_location = image_service.get_location(context, image_id)
cloned = self.driver.clone_image(volume_ref, image_location)
if not cloned:
model_update = self.driver.create_volume(volume_ref)
status = 'downloading'
if model_update:
self.db.volume_update(context, volume_ref['id'], model_update)
@ -148,7 +157,7 @@ class VolumeManager(manager.SchedulerDependentManager):
LOG.debug(_("volume %s: created successfully"), volume_ref['name'])
self._reset_stats()
if image_id:
if image_id and not cloned:
#copy the image onto the volume.
self._copy_image_to_volume(context, volume_ref, image_id)
return volume_ref['id']