]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Do not clone non-raw images in rbd backend
authorDmitry Borodaenko <angdraug@gmail.com>
Wed, 27 Nov 2013 22:33:00 +0000 (14:33 -0800)
committerDmitry Borodaenko <dborodaenko@mirantis.com>
Mon, 16 Dec 2013 02:24:28 +0000 (18:24 -0800)
RBD backend only supports booting from images in raw format. A volume
that was cloned from an image in any other format is not bootable. The
RBD driver will consider non-raw images to be uncloneable to trigger
automatic conversion to raw format.

Includes conversion of the corresponding unit test to use mock (instead
of mox) and expanded comments and error messages based on change #58893
by Edward Hope-Morley.

Change-Id: I5725d2f7576bc1b3e9b874ba944ad17d33a6e2cb
Closes-Bug: #1246219
Closes-Bug: #1247998

cinder/tests/test_gpfs.py
cinder/tests/test_netapp_nfs.py
cinder/tests/test_rbd.py
cinder/tests/test_volume.py
cinder/volume/driver.py
cinder/volume/drivers/gpfs.py
cinder/volume/drivers/lvm.py
cinder/volume/drivers/netapp/nfs.py
cinder/volume/drivers/rbd.py
cinder/volume/drivers/scality.py
cinder/volume/flows/create_volume/__init__.py

index 3dd7eaf1ea33a40d26bc84901d81bf83792f8b9a..adb87c0e51844ecf613056d608cd15af7fd52cd6 100644 (file)
@@ -291,7 +291,8 @@ class GPFSDriverTestCase(test.TestCase):
         CONF.gpfs_images_share_mode = 'copy_on_write'
         self.driver.clone_image(volume,
                                 None,
-                                self.image_id)
+                                self.image_id,
+                                {})
 
         self.assertTrue(os.path.exists(volumepath))
         self.volume.delete_volume(self.context, volume['id'])
@@ -312,7 +313,8 @@ class GPFSDriverTestCase(test.TestCase):
         CONF.gpfs_images_share_mode = 'copy'
         self.driver.clone_image(volume,
                                 None,
-                                self.image_id)
+                                self.image_id,
+                                {})
 
         self.assertTrue(os.path.exists(volumepath))
         self.volume.delete_volume(self.context, volume['id'])
index 397e671c0cd377e9c59bc684aac3ab8d2bb255d5..a7f3b6841e7644f617e2621d0b6e464d71b6f16d 100644 (file)
@@ -481,7 +481,7 @@ class NetappDirectCmodeNfsDriverTestCase(test.TestCase):
         drv._post_clone_image(volume)
 
         mox.ReplayAll()
-        drv. clone_image(volume, ('image_location', None), 'image_id')
+        drv.clone_image(volume, ('image_location', None), 'image_id', {})
         mox.VerifyAll()
 
     def get_img_info(self, format):
@@ -505,7 +505,7 @@ class NetappDirectCmodeNfsDriverTestCase(test.TestCase):
 
         mox.ReplayAll()
         (prop, cloned) = drv. clone_image(
-            volume, ('nfs://127.0.0.1:/share/img-id', None), 'image_id')
+            volume, ('nfs://127.0.0.1:/share/img-id', None), 'image_id', {})
         mox.VerifyAll()
         if not cloned and not prop['provider_location']:
             pass
@@ -541,7 +541,7 @@ class NetappDirectCmodeNfsDriverTestCase(test.TestCase):
 
         mox.ReplayAll()
         drv. clone_image(
-            volume, ('nfs://127.0.0.1:/share/img-id', None), 'image_id')
+            volume, ('nfs://127.0.0.1:/share/img-id', None), 'image_id', {})
         mox.VerifyAll()
 
     def test_clone_image_cloneableshare_notraw(self):
@@ -578,7 +578,7 @@ class NetappDirectCmodeNfsDriverTestCase(test.TestCase):
 
         mox.ReplayAll()
         drv. clone_image(
-            volume, ('nfs://127.0.0.1/share/img-id', None), 'image_id')
+            volume, ('nfs://127.0.0.1/share/img-id', None), 'image_id', {})
         mox.VerifyAll()
 
     def test_clone_image_file_not_discovered(self):
@@ -617,7 +617,7 @@ class NetappDirectCmodeNfsDriverTestCase(test.TestCase):
 
         mox.ReplayAll()
         vol_dict, result = drv. clone_image(
-            volume, ('nfs://127.0.0.1/share/img-id', None), 'image_id')
+            volume, ('nfs://127.0.0.1/share/img-id', None), 'image_id', {})
         mox.VerifyAll()
         self.assertFalse(result)
         self.assertFalse(vol_dict['bootable'])
@@ -664,7 +664,7 @@ class NetappDirectCmodeNfsDriverTestCase(test.TestCase):
 
         mox.ReplayAll()
         vol_dict, result = drv. clone_image(
-            volume, ('nfs://127.0.0.1/share/img-id', None), 'image_id')
+            volume, ('nfs://127.0.0.1/share/img-id', None), 'image_id', {})
         mox.VerifyAll()
         self.assertFalse(result)
         self.assertFalse(vol_dict['bootable'])
index 7b05ed1aba6a384fccf832e996ddd082a5aca61d..43ab261628cc5b91e6ccb96a05c98a0ba01e6d01 100644 (file)
@@ -35,6 +35,7 @@ from cinder.tests.test_volume import DriverTestCase
 from cinder import units
 from cinder.volume import configuration as conf
 import cinder.volume.drivers.rbd as driver
+from cinder.volume.flows import create_volume
 
 
 LOG = logging.getLogger(__name__)
@@ -310,7 +311,8 @@ class RBDTestCase(test.TestCase):
             self.assertRaises(exception.ImageUnacceptable,
                               self.driver._parse_location,
                               loc)
-            self.assertFalse(self.driver._is_cloneable(loc))
+            self.assertFalse(
+                self.driver._is_cloneable(loc, {'disk_format': 'raw'}))
 
     def test_cloneable(self):
         self.stubs.Set(self.driver, '_get_fsid', lambda: 'abc')
@@ -327,12 +329,14 @@ class RBDTestCase(test.TestCase):
 
         self.mox.ReplayAll()
 
-        self.assertTrue(self.driver._is_cloneable(location))
+        self.assertTrue(
+            self.driver._is_cloneable(location, {'disk_format': 'raw'}))
 
     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))
+        self.assertFalse(
+            self.driver._is_cloneable(location, {'disk_format': 'raw'}))
 
     def test_uncloneable_unreadable(self):
         self.stubs.Set(self.driver, '_get_fsid', lambda: 'abc')
@@ -347,7 +351,16 @@ class RBDTestCase(test.TestCase):
 
         self.mox.ReplayAll()
 
-        self.assertFalse(self.driver._is_cloneable(location))
+        self.assertFalse(
+            self.driver._is_cloneable(location, {'disk_format': 'raw'}))
+
+    def test_uncloneable_bad_format(self):
+        self.stubs.Set(self.driver, '_get_fsid', lambda: 'abc')
+        location = 'rbd://abc/pool/image/snap'
+        formats = ['qcow2', 'vmdk', 'vdi']
+        for f in formats:
+            self.assertFalse(
+                self.driver._is_cloneable(location, {'disk_format': f}))
 
     def _copy_image(self):
         @contextlib.contextmanager
@@ -567,26 +580,31 @@ class ManagedRBDTestCase(DriverTestCase):
         super(ManagedRBDTestCase, self).setUp()
         fake_image.stub_out_image_service(self.stubs)
         self.volume.driver.set_initialized()
+        self.called = []
 
-    def _clone_volume_from_image(self, expected_status,
-                                 clone_works=True):
+    def _create_volume_from_image(self, expected_status, raw=False,
+                                  clone_error=False):
         """Try to clone a volume from an image, and check the status
         afterwards.
-        """
-        def fake_clone_image(volume, image_location, image_id):
-            return {'provider_location': None}, True
 
-        def fake_clone_error(volume, image_location, image_id):
-            raise exception.CinderException()
+        NOTE: if clone_error is True we force the image type to raw otherwise
+              clone_image is not called
+        """
+        def mock_clone_image(volume, image_location, image_id, image_meta):
+            self.called.append('clone_image')
+            if clone_error:
+                raise exception.CinderException()
+            else:
+                return {'provider_location': None}, True
 
-        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)
+        # See tests.image.fake for image types.
+        if raw:
+            image_id = '155d900f-4e14-4e4c-a73d-069cbf4541e6'
         else:
-            self.stubs.Set(self.volume.driver, 'clone_image', fake_clone_error)
+            image_id = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
 
-        image_id = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
         volume_id = 1
+
         # creating volume testdata
         db.volume_create(self.context,
                          {'id': volume_id,
@@ -596,58 +614,72 @@ class ManagedRBDTestCase(DriverTestCase):
                           '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_create_vol_from_image_status_available(self):
-        """Verify that before cloning, an image is in the available state."""
-        self._clone_volume_from_image('available', True)
-
-    def test_create_vol_from_image_status_error(self):
-        """Verify that before cloning, an image is in the available state."""
-        self._clone_volume_from_image('error', False)
+        mpo = mock.patch.object
+        with mpo(self.volume.driver, 'create_volume') as mock_create_volume:
+            with mpo(self.volume.driver, 'clone_image', mock_clone_image):
+                with mpo(create_volume.CreateVolumeFromSpecTask,
+                         '_copy_image_to_volume') as mock_copy_image_to_volume:
+
+                    try:
+                        if not clone_error:
+                            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)
+
+                    self.assertEqual(self.called, ['clone_image'])
+                    mock_create_volume.assert_called()
+                    mock_copy_image_to_volume.assert_called()
 
-    def test_clone_image(self):
-        # Test Failure Case(s)
-        expected = ({}, False)
+    def test_create_vol_from_image_status_available(self):
+        """Clone raw image then verify volume is in available state."""
+        self._create_volume_from_image('available', raw=True)
 
-        self.stubs.Set(self.volume.driver, '_is_cloneable', lambda x: False)
-        image_loc = (object(), object())
-        actual = self.volume.driver.clone_image(object(), image_loc, object())
-        self.assertEqual(expected, actual)
+    def test_create_vol_from_non_raw_image_status_available(self):
+        """Clone non-raw image then verify volume is in available state."""
+        self._create_volume_from_image('available', raw=False)
 
-        self.stubs.Set(self.volume.driver, '_is_cloneable', lambda x: True)
-        self.assertEqual(expected,
-                         self.volume.driver.clone_image(object(), None, None))
+    def test_create_vol_from_image_status_error(self):
+        """Fail to clone raw image then verify volume is in error state."""
+        self._create_volume_from_image('error', raw=True, clone_error=True)
 
-        # Test Success Case(s)
-        expected = ({'provider_location': None}, True)
+    def test_clone_failure(self):
+        driver = self.volume.driver
 
-        self.stubs.Set(self.volume.driver, '_parse_location',
-                       lambda x: ('a', 'b', 'c', 'd'))
+        with mock.patch.object(driver, '_is_cloneable', lambda *args: False):
+            image_loc = (mock.Mock(), mock.Mock())
+            actual = driver.clone_image(mock.Mock(), image_loc,
+                                        mock.Mock(), {})
+            self.assertEqual(({}, False), actual)
 
-        self.stubs.Set(self.volume.driver, '_clone', lambda *args: None)
-        self.stubs.Set(self.volume.driver, '_resize', lambda *args: None)
-        actual = self.volume.driver.clone_image(object(), image_loc, object())
-        self.assertEqual(expected, actual)
+        self.assertEqual(({}, False),
+                         driver.clone_image(object(), None, None, {}))
 
     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, c: True)
-        image_id = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
-        self.assertTrue(self.volume.driver.clone_image({}, image_id, image_id))
+        expected = ({'provider_location': None}, True)
+        driver = self.volume.driver
+        mpo = mock.patch.object
+        with mpo(driver, '_is_cloneable', lambda *args: True):
+            with mpo(driver, '_parse_location', lambda x: (1, 2, 3, 4)):
+                with mpo(driver, '_clone') as mock_clone:
+                    with mpo(driver, '_resize') as mock_resize:
+                        image_loc = (mock.Mock(), mock.Mock())
+                        actual = driver.clone_image(mock.Mock(),
+                                                    image_loc,
+                                                    mock.Mock(),
+                                                    {'disk_format': 'raw'})
+                        self.assertEqual(expected, actual)
+                        mock_clone.assert_called()
+                        mock_resize.assert_called()
index 16057af31514ed5af197a69a826ff705c8f15288..11d5b28c42bbd5b22f9fdc40a073ea89a40ebd0a 100644 (file)
@@ -1486,7 +1486,7 @@ class VolumeTestCase(BaseVolumeTestCase):
         def fake_fetch_to_raw(ctx, image_service, image_id, path, size=None):
             pass
 
-        def fake_clone_image(volume_ref, image_location, image_id):
+        def fake_clone_image(volume_ref, image_location, image_id, image_meta):
             return {'provider_location': None}, True
 
         dst_fd, dst_path = tempfile.mkstemp()
index 67c3585573921fed86e6fa2d661ee4f4180bc777..45bd4178381e7ea06a0e37e50fd4c65531a6c851 100644 (file)
@@ -400,7 +400,7 @@ class VolumeDriver(object):
         connector.disconnect_volume(attach_info['conn']['data'],
                                     attach_info['device'])
 
-    def clone_image(self, volume, image_location, image_id):
+    def clone_image(self, volume, image_location, image_id, image_meta):
         """Create a volume efficiently from an existing image.
 
         image_location is a string whose format depends on the
@@ -411,6 +411,11 @@ class VolumeDriver(object):
         It can be used by the driver to introspect internal
         stores or registry to do an efficient image clone.
 
+        image_meta is a dictionary that includes 'disk_format' (e.g.
+        raw, qcow2) and other image attributes that allow drivers to
+        decide whether they can clone the image without first requiring
+        conversion.
+
         Returns a dict of volume properties eg. provider_location,
         boolean indicating whether cloning occurred
         """
index 407bd595633d9055cd5fe71f565a3cbf2eb7175c..97cdafd911e7cf66325336c3f0702f1057ef8ab1 100644 (file)
@@ -463,7 +463,7 @@ class GPFSDriver(driver.VolumeDriver):
             return '100M'
         return '%sG' % size_in_g
 
-    def clone_image(self, volume, image_location, image_id):
+    def clone_image(self, volume, image_location, image_id, image_meta):
         return self._clone_image(volume, image_location, image_id)
 
     def _is_cloneable(self, image_id):
index 3529fe95536f7f7654a045cc37a2e2029a1e8590..35c7968624ee76fb70b6d9f387786ebf3556c45e 100644 (file)
@@ -317,7 +317,7 @@ class LVMVolumeDriver(driver.VolumeDriver):
         finally:
             self.delete_snapshot(temp_snapshot)
 
-    def clone_image(self, volume, image_location, image_id):
+    def clone_image(self, volume, image_location, image_id, image_meta):
         return None, False
 
     def backup_volume(self, context, backup, backup_service):
index 2c3dfba32cf6f054c820c5d0106f29d454ddaee9..31b8667ae085eeeb78976dd5a1410ff84989ae59 100644 (file)
@@ -374,7 +374,7 @@ class NetAppNFSDriver(nfs.NfsDriver):
             LOG.warning(_('Exception during deleting %s'), ex.__str__())
             return False
 
-    def clone_image(self, volume, image_location, image_id):
+    def clone_image(self, volume, image_location, image_id, image_meta):
         """Create a volume efficiently from an existing image.
 
         image_location is a string whose format depends on the
index 88ffe0d1b465ec697c472263fcc3ce7810ca4f3e..95d2ec6971a3c215251321502b6a854697bc17c4 100644 (file)
@@ -717,7 +717,7 @@ class RBDDriver(driver.VolumeDriver):
         with RADOSClient(self) as client:
             return client.cluster.get_fsid()
 
-    def _is_cloneable(self, image_location):
+    def _is_cloneable(self, image_location, image_meta):
         try:
             fsid, pool, image, snapshot = self._parse_location(image_location)
         except exception.ImageUnacceptable as e:
@@ -729,6 +729,13 @@ class RBDDriver(driver.VolumeDriver):
             LOG.debug(reason)
             return False
 
+        if image_meta['disk_format'] != 'raw':
+            reason = _("rbd image clone requires image format to be "
+                       "'raw' but image {0} is '{1}'").format(
+                           image_location, image_meta['disk_format'])
+            LOG.debug(reason)
+            return False
+
         # check that we can read the image
         try:
             with RBDVolumeProxy(self, image,
@@ -741,9 +748,10 @@ class RBDDriver(driver.VolumeDriver):
                       dict(loc=image_location, err=e))
             return False
 
-    def clone_image(self, volume, image_location, image_id):
+    def clone_image(self, volume, image_location, image_id, image_meta):
         image_location = image_location[0] if image_location else None
-        if image_location is None or not self._is_cloneable(image_location):
+        if image_location is None or not self._is_cloneable(
+                image_location, image_meta):
             return ({}, False)
         prefix, pool, image, snapshot = self._parse_location(image_location)
         self._clone(volume, pool, image, snapshot)
index 4cf49c6b731e072c105e518496759160a039b4b7..abd6c2910d680c07c3b51fbea79ffe830900470a 100644 (file)
@@ -250,7 +250,7 @@ class ScalityDriver(driver.VolumeDriver):
                                   image_meta,
                                   self.local_path(volume))
 
-    def clone_image(self, volume, image_location, image_id):
+    def clone_image(self, volume, image_location, image_id, image_meta):
         """Create a volume efficiently from an existing image.
 
         image_location is a string whose format depends on the
index 7f3a6efa28a8655aff0e73831d2a40dd45cf703a..5984b0ee81c03233d1101da560ebc327aac16e26 100644 (file)
@@ -1364,7 +1364,7 @@ class CreateVolumeFromSpecTask(base.CinderTask):
         # dict containing provider_location for cloned volume
         # and clone status.
         model_update, cloned = self.driver.clone_image(
-            volume_ref, image_location, image_id)
+            volume_ref, image_location, image_id, image_meta)
         if not cloned:
             # TODO(harlowja): what needs to be rolled back in the clone if this
             # volume create fails?? Likely this should be a subflow or broken