]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
add ability to clone images
authorJosh Durgin <josh.durgin@inktank.com>
Tue, 14 Aug 2012 19:27:48 +0000 (12:27 -0700)
committerJosh Durgin <josh.durgin@inktank.com>
Wed, 15 Aug 2012 21:32:19 +0000 (14:32 -0700)
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>
cinder/tests/test_rbd.py [new file with mode: 0644]
cinder/volume/api.py
cinder/volume/driver.py
cinder/volume/manager.py

diff --git a/cinder/tests/test_rbd.py b/cinder/tests/test_rbd.py
new file mode 100644 (file)
index 0000000..704e180
--- /dev/null
@@ -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'))
index a562c95ec1c54bf19c40d1d4c80ab8b7c45c8ee2..d97d4886d40451564709d1e1d5ade0250b472688 100644 (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.')
index 8e56cca4ed498cd26e856058556c52aa918d67a5..177d413a400e8163d0ec26d3f207bc03a57bc76e 100644 (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"""
index 1a7570ddad6448d72b4365316b56a831a28346af..a40948f175169a417eebd2a7dfe297e227d88d70 100644 (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']