Browse Source

Do not clone non-raw images in rbd backend

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
tags/2014.1.b2
Dmitry Borodaenko 5 years ago
parent
commit
e066158b52

+ 4
- 2
cinder/tests/test_gpfs.py View File

@@ -291,7 +291,8 @@ class GPFSDriverTestCase(test.TestCase):
291 291
         CONF.gpfs_images_share_mode = 'copy_on_write'
292 292
         self.driver.clone_image(volume,
293 293
                                 None,
294
-                                self.image_id)
294
+                                self.image_id,
295
+                                {})
295 296
 
296 297
         self.assertTrue(os.path.exists(volumepath))
297 298
         self.volume.delete_volume(self.context, volume['id'])
@@ -312,7 +313,8 @@ class GPFSDriverTestCase(test.TestCase):
312 313
         CONF.gpfs_images_share_mode = 'copy'
313 314
         self.driver.clone_image(volume,
314 315
                                 None,
315
-                                self.image_id)
316
+                                self.image_id,
317
+                                {})
316 318
 
317 319
         self.assertTrue(os.path.exists(volumepath))
318 320
         self.volume.delete_volume(self.context, volume['id'])

+ 6
- 6
cinder/tests/test_netapp_nfs.py View File

@@ -481,7 +481,7 @@ class NetappDirectCmodeNfsDriverTestCase(test.TestCase):
481 481
         drv._post_clone_image(volume)
482 482
 
483 483
         mox.ReplayAll()
484
-        drv. clone_image(volume, ('image_location', None), 'image_id')
484
+        drv.clone_image(volume, ('image_location', None), 'image_id', {})
485 485
         mox.VerifyAll()
486 486
 
487 487
     def get_img_info(self, format):
@@ -505,7 +505,7 @@ class NetappDirectCmodeNfsDriverTestCase(test.TestCase):
505 505
 
506 506
         mox.ReplayAll()
507 507
         (prop, cloned) = drv. clone_image(
508
-            volume, ('nfs://127.0.0.1:/share/img-id', None), 'image_id')
508
+            volume, ('nfs://127.0.0.1:/share/img-id', None), 'image_id', {})
509 509
         mox.VerifyAll()
510 510
         if not cloned and not prop['provider_location']:
511 511
             pass
@@ -541,7 +541,7 @@ class NetappDirectCmodeNfsDriverTestCase(test.TestCase):
541 541
 
542 542
         mox.ReplayAll()
543 543
         drv. clone_image(
544
-            volume, ('nfs://127.0.0.1:/share/img-id', None), 'image_id')
544
+            volume, ('nfs://127.0.0.1:/share/img-id', None), 'image_id', {})
545 545
         mox.VerifyAll()
546 546
 
547 547
     def test_clone_image_cloneableshare_notraw(self):
@@ -578,7 +578,7 @@ class NetappDirectCmodeNfsDriverTestCase(test.TestCase):
578 578
 
579 579
         mox.ReplayAll()
580 580
         drv. clone_image(
581
-            volume, ('nfs://127.0.0.1/share/img-id', None), 'image_id')
581
+            volume, ('nfs://127.0.0.1/share/img-id', None), 'image_id', {})
582 582
         mox.VerifyAll()
583 583
 
584 584
     def test_clone_image_file_not_discovered(self):
@@ -617,7 +617,7 @@ class NetappDirectCmodeNfsDriverTestCase(test.TestCase):
617 617
 
618 618
         mox.ReplayAll()
619 619
         vol_dict, result = drv. clone_image(
620
-            volume, ('nfs://127.0.0.1/share/img-id', None), 'image_id')
620
+            volume, ('nfs://127.0.0.1/share/img-id', None), 'image_id', {})
621 621
         mox.VerifyAll()
622 622
         self.assertFalse(result)
623 623
         self.assertFalse(vol_dict['bootable'])
@@ -664,7 +664,7 @@ class NetappDirectCmodeNfsDriverTestCase(test.TestCase):
664 664
 
665 665
         mox.ReplayAll()
666 666
         vol_dict, result = drv. clone_image(
667
-            volume, ('nfs://127.0.0.1/share/img-id', None), 'image_id')
667
+            volume, ('nfs://127.0.0.1/share/img-id', None), 'image_id', {})
668 668
         mox.VerifyAll()
669 669
         self.assertFalse(result)
670 670
         self.assertFalse(vol_dict['bootable'])

+ 94
- 62
cinder/tests/test_rbd.py View File

@@ -35,6 +35,7 @@ from cinder.tests.test_volume import DriverTestCase
35 35
 from cinder import units
36 36
 from cinder.volume import configuration as conf
37 37
 import cinder.volume.drivers.rbd as driver
38
+from cinder.volume.flows import create_volume
38 39
 
39 40
 
40 41
 LOG = logging.getLogger(__name__)
@@ -310,7 +311,8 @@ class RBDTestCase(test.TestCase):
310 311
             self.assertRaises(exception.ImageUnacceptable,
311 312
                               self.driver._parse_location,
312 313
                               loc)
313
-            self.assertFalse(self.driver._is_cloneable(loc))
314
+            self.assertFalse(
315
+                self.driver._is_cloneable(loc, {'disk_format': 'raw'}))
314 316
 
315 317
     def test_cloneable(self):
316 318
         self.stubs.Set(self.driver, '_get_fsid', lambda: 'abc')
@@ -327,12 +329,14 @@ class RBDTestCase(test.TestCase):
327 329
 
328 330
         self.mox.ReplayAll()
329 331
 
330
-        self.assertTrue(self.driver._is_cloneable(location))
332
+        self.assertTrue(
333
+            self.driver._is_cloneable(location, {'disk_format': 'raw'}))
331 334
 
332 335
     def test_uncloneable_different_fsid(self):
333 336
         self.stubs.Set(self.driver, '_get_fsid', lambda: 'abc')
334 337
         location = 'rbd://def/pool/image/snap'
335
-        self.assertFalse(self.driver._is_cloneable(location))
338
+        self.assertFalse(
339
+            self.driver._is_cloneable(location, {'disk_format': 'raw'}))
336 340
 
337 341
     def test_uncloneable_unreadable(self):
338 342
         self.stubs.Set(self.driver, '_get_fsid', lambda: 'abc')
@@ -347,7 +351,16 @@ class RBDTestCase(test.TestCase):
347 351
 
348 352
         self.mox.ReplayAll()
349 353
 
350
-        self.assertFalse(self.driver._is_cloneable(location))
354
+        self.assertFalse(
355
+            self.driver._is_cloneable(location, {'disk_format': 'raw'}))
356
+
357
+    def test_uncloneable_bad_format(self):
358
+        self.stubs.Set(self.driver, '_get_fsid', lambda: 'abc')
359
+        location = 'rbd://abc/pool/image/snap'
360
+        formats = ['qcow2', 'vmdk', 'vdi']
361
+        for f in formats:
362
+            self.assertFalse(
363
+                self.driver._is_cloneable(location, {'disk_format': f}))
351 364
 
352 365
     def _copy_image(self):
353 366
         @contextlib.contextmanager
@@ -567,26 +580,31 @@ class ManagedRBDTestCase(DriverTestCase):
567 580
         super(ManagedRBDTestCase, self).setUp()
568 581
         fake_image.stub_out_image_service(self.stubs)
569 582
         self.volume.driver.set_initialized()
583
+        self.called = []
570 584
 
571
-    def _clone_volume_from_image(self, expected_status,
572
-                                 clone_works=True):
585
+    def _create_volume_from_image(self, expected_status, raw=False,
586
+                                  clone_error=False):
573 587
         """Try to clone a volume from an image, and check the status
574 588
         afterwards.
575
-        """
576
-        def fake_clone_image(volume, image_location, image_id):
577
-            return {'provider_location': None}, True
578 589
 
579
-        def fake_clone_error(volume, image_location, image_id):
580
-            raise exception.CinderException()
590
+        NOTE: if clone_error is True we force the image type to raw otherwise
591
+              clone_image is not called
592
+        """
593
+        def mock_clone_image(volume, image_location, image_id, image_meta):
594
+            self.called.append('clone_image')
595
+            if clone_error:
596
+                raise exception.CinderException()
597
+            else:
598
+                return {'provider_location': None}, True
581 599
 
582
-        self.stubs.Set(self.volume.driver, '_is_cloneable', lambda x: True)
583
-        if clone_works:
584
-            self.stubs.Set(self.volume.driver, 'clone_image', fake_clone_image)
600
+        # See tests.image.fake for image types.
601
+        if raw:
602
+            image_id = '155d900f-4e14-4e4c-a73d-069cbf4541e6'
585 603
         else:
586
-            self.stubs.Set(self.volume.driver, 'clone_image', fake_clone_error)
604
+            image_id = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
587 605
 
588
-        image_id = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
589 606
         volume_id = 1
607
+
590 608
         # creating volume testdata
591 609
         db.volume_create(self.context,
592 610
                          {'id': volume_id,
@@ -596,58 +614,72 @@ class ManagedRBDTestCase(DriverTestCase):
596 614
                           'status': 'creating',
597 615
                           'instance_uuid': None,
598 616
                           'host': 'dummy'})
599
-        try:
600
-            if clone_works:
601
-                self.volume.create_volume(self.context,
602
-                                          volume_id,
603
-                                          image_id=image_id)
604
-            else:
605
-                self.assertRaises(exception.CinderException,
606
-                                  self.volume.create_volume,
607
-                                  self.context,
608
-                                  volume_id,
609
-                                  image_id=image_id)
610
-
611
-            volume = db.volume_get(self.context, volume_id)
612
-            self.assertEqual(volume['status'], expected_status)
613
-        finally:
614
-            # cleanup
615
-            db.volume_destroy(self.context, volume_id)
616 617
 
617
-    def test_create_vol_from_image_status_available(self):
618
-        """Verify that before cloning, an image is in the available state."""
619
-        self._clone_volume_from_image('available', True)
620
-
621
-    def test_create_vol_from_image_status_error(self):
622
-        """Verify that before cloning, an image is in the available state."""
623
-        self._clone_volume_from_image('error', False)
618
+        mpo = mock.patch.object
619
+        with mpo(self.volume.driver, 'create_volume') as mock_create_volume:
620
+            with mpo(self.volume.driver, 'clone_image', mock_clone_image):
621
+                with mpo(create_volume.CreateVolumeFromSpecTask,
622
+                         '_copy_image_to_volume') as mock_copy_image_to_volume:
623
+
624
+                    try:
625
+                        if not clone_error:
626
+                            self.volume.create_volume(self.context,
627
+                                                      volume_id,
628
+                                                      image_id=image_id)
629
+                        else:
630
+                            self.assertRaises(exception.CinderException,
631
+                                              self.volume.create_volume,
632
+                                              self.context,
633
+                                              volume_id,
634
+                                              image_id=image_id)
635
+
636
+                        volume = db.volume_get(self.context, volume_id)
637
+                        self.assertEqual(volume['status'], expected_status)
638
+                    finally:
639
+                        # cleanup
640
+                        db.volume_destroy(self.context, volume_id)
641
+
642
+                    self.assertEqual(self.called, ['clone_image'])
643
+                    mock_create_volume.assert_called()
644
+                    mock_copy_image_to_volume.assert_called()
624 645
 
625
-    def test_clone_image(self):
626
-        # Test Failure Case(s)
627
-        expected = ({}, False)
646
+    def test_create_vol_from_image_status_available(self):
647
+        """Clone raw image then verify volume is in available state."""
648
+        self._create_volume_from_image('available', raw=True)
628 649
 
629
-        self.stubs.Set(self.volume.driver, '_is_cloneable', lambda x: False)
630
-        image_loc = (object(), object())
631
-        actual = self.volume.driver.clone_image(object(), image_loc, object())
632
-        self.assertEqual(expected, actual)
650
+    def test_create_vol_from_non_raw_image_status_available(self):
651
+        """Clone non-raw image then verify volume is in available state."""
652
+        self._create_volume_from_image('available', raw=False)
633 653
 
634
-        self.stubs.Set(self.volume.driver, '_is_cloneable', lambda x: True)
635
-        self.assertEqual(expected,
636
-                         self.volume.driver.clone_image(object(), None, None))
654
+    def test_create_vol_from_image_status_error(self):
655
+        """Fail to clone raw image then verify volume is in error state."""
656
+        self._create_volume_from_image('error', raw=True, clone_error=True)
637 657
 
638
-        # Test Success Case(s)
639
-        expected = ({'provider_location': None}, True)
658
+    def test_clone_failure(self):
659
+        driver = self.volume.driver
640 660
 
641
-        self.stubs.Set(self.volume.driver, '_parse_location',
642
-                       lambda x: ('a', 'b', 'c', 'd'))
661
+        with mock.patch.object(driver, '_is_cloneable', lambda *args: False):
662
+            image_loc = (mock.Mock(), mock.Mock())
663
+            actual = driver.clone_image(mock.Mock(), image_loc,
664
+                                        mock.Mock(), {})
665
+            self.assertEqual(({}, False), actual)
643 666
 
644
-        self.stubs.Set(self.volume.driver, '_clone', lambda *args: None)
645
-        self.stubs.Set(self.volume.driver, '_resize', lambda *args: None)
646
-        actual = self.volume.driver.clone_image(object(), image_loc, object())
647
-        self.assertEqual(expected, actual)
667
+        self.assertEqual(({}, False),
668
+                         driver.clone_image(object(), None, None, {}))
648 669
 
649 670
     def test_clone_success(self):
650
-        self.stubs.Set(self.volume.driver, '_is_cloneable', lambda x: True)
651
-        self.stubs.Set(self.volume.driver, 'clone_image', lambda a, b, c: True)
652
-        image_id = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
653
-        self.assertTrue(self.volume.driver.clone_image({}, image_id, image_id))
671
+        expected = ({'provider_location': None}, True)
672
+        driver = self.volume.driver
673
+        mpo = mock.patch.object
674
+        with mpo(driver, '_is_cloneable', lambda *args: True):
675
+            with mpo(driver, '_parse_location', lambda x: (1, 2, 3, 4)):
676
+                with mpo(driver, '_clone') as mock_clone:
677
+                    with mpo(driver, '_resize') as mock_resize:
678
+                        image_loc = (mock.Mock(), mock.Mock())
679
+                        actual = driver.clone_image(mock.Mock(),
680
+                                                    image_loc,
681
+                                                    mock.Mock(),
682
+                                                    {'disk_format': 'raw'})
683
+                        self.assertEqual(expected, actual)
684
+                        mock_clone.assert_called()
685
+                        mock_resize.assert_called()

+ 1
- 1
cinder/tests/test_volume.py View File

@@ -1486,7 +1486,7 @@ class VolumeTestCase(BaseVolumeTestCase):
1486 1486
         def fake_fetch_to_raw(ctx, image_service, image_id, path, size=None):
1487 1487
             pass
1488 1488
 
1489
-        def fake_clone_image(volume_ref, image_location, image_id):
1489
+        def fake_clone_image(volume_ref, image_location, image_id, image_meta):
1490 1490
             return {'provider_location': None}, True
1491 1491
 
1492 1492
         dst_fd, dst_path = tempfile.mkstemp()

+ 6
- 1
cinder/volume/driver.py View File

@@ -400,7 +400,7 @@ class VolumeDriver(object):
400 400
         connector.disconnect_volume(attach_info['conn']['data'],
401 401
                                     attach_info['device'])
402 402
 
403
-    def clone_image(self, volume, image_location, image_id):
403
+    def clone_image(self, volume, image_location, image_id, image_meta):
404 404
         """Create a volume efficiently from an existing image.
405 405
 
406 406
         image_location is a string whose format depends on the
@@ -411,6 +411,11 @@ class VolumeDriver(object):
411 411
         It can be used by the driver to introspect internal
412 412
         stores or registry to do an efficient image clone.
413 413
 
414
+        image_meta is a dictionary that includes 'disk_format' (e.g.
415
+        raw, qcow2) and other image attributes that allow drivers to
416
+        decide whether they can clone the image without first requiring
417
+        conversion.
418
+
414 419
         Returns a dict of volume properties eg. provider_location,
415 420
         boolean indicating whether cloning occurred
416 421
         """

+ 1
- 1
cinder/volume/drivers/gpfs.py View File

@@ -463,7 +463,7 @@ class GPFSDriver(driver.VolumeDriver):
463 463
             return '100M'
464 464
         return '%sG' % size_in_g
465 465
 
466
-    def clone_image(self, volume, image_location, image_id):
466
+    def clone_image(self, volume, image_location, image_id, image_meta):
467 467
         return self._clone_image(volume, image_location, image_id)
468 468
 
469 469
     def _is_cloneable(self, image_id):

+ 1
- 1
cinder/volume/drivers/lvm.py View File

@@ -317,7 +317,7 @@ class LVMVolumeDriver(driver.VolumeDriver):
317 317
         finally:
318 318
             self.delete_snapshot(temp_snapshot)
319 319
 
320
-    def clone_image(self, volume, image_location, image_id):
320
+    def clone_image(self, volume, image_location, image_id, image_meta):
321 321
         return None, False
322 322
 
323 323
     def backup_volume(self, context, backup, backup_service):

+ 1
- 1
cinder/volume/drivers/netapp/nfs.py View File

@@ -374,7 +374,7 @@ class NetAppNFSDriver(nfs.NfsDriver):
374 374
             LOG.warning(_('Exception during deleting %s'), ex.__str__())
375 375
             return False
376 376
 
377
-    def clone_image(self, volume, image_location, image_id):
377
+    def clone_image(self, volume, image_location, image_id, image_meta):
378 378
         """Create a volume efficiently from an existing image.
379 379
 
380 380
         image_location is a string whose format depends on the

+ 11
- 3
cinder/volume/drivers/rbd.py View File

@@ -717,7 +717,7 @@ class RBDDriver(driver.VolumeDriver):
717 717
         with RADOSClient(self) as client:
718 718
             return client.cluster.get_fsid()
719 719
 
720
-    def _is_cloneable(self, image_location):
720
+    def _is_cloneable(self, image_location, image_meta):
721 721
         try:
722 722
             fsid, pool, image, snapshot = self._parse_location(image_location)
723 723
         except exception.ImageUnacceptable as e:
@@ -729,6 +729,13 @@ class RBDDriver(driver.VolumeDriver):
729 729
             LOG.debug(reason)
730 730
             return False
731 731
 
732
+        if image_meta['disk_format'] != 'raw':
733
+            reason = _("rbd image clone requires image format to be "
734
+                       "'raw' but image {0} is '{1}'").format(
735
+                           image_location, image_meta['disk_format'])
736
+            LOG.debug(reason)
737
+            return False
738
+
732 739
         # check that we can read the image
733 740
         try:
734 741
             with RBDVolumeProxy(self, image,
@@ -741,9 +748,10 @@ class RBDDriver(driver.VolumeDriver):
741 748
                       dict(loc=image_location, err=e))
742 749
             return False
743 750
 
744
-    def clone_image(self, volume, image_location, image_id):
751
+    def clone_image(self, volume, image_location, image_id, image_meta):
745 752
         image_location = image_location[0] if image_location else None
746
-        if image_location is None or not self._is_cloneable(image_location):
753
+        if image_location is None or not self._is_cloneable(
754
+                image_location, image_meta):
747 755
             return ({}, False)
748 756
         prefix, pool, image, snapshot = self._parse_location(image_location)
749 757
         self._clone(volume, pool, image, snapshot)

+ 1
- 1
cinder/volume/drivers/scality.py View File

@@ -250,7 +250,7 @@ class ScalityDriver(driver.VolumeDriver):
250 250
                                   image_meta,
251 251
                                   self.local_path(volume))
252 252
 
253
-    def clone_image(self, volume, image_location, image_id):
253
+    def clone_image(self, volume, image_location, image_id, image_meta):
254 254
         """Create a volume efficiently from an existing image.
255 255
 
256 256
         image_location is a string whose format depends on the

+ 1
- 1
cinder/volume/flows/create_volume/__init__.py View File

@@ -1364,7 +1364,7 @@ class CreateVolumeFromSpecTask(base.CinderTask):
1364 1364
         # dict containing provider_location for cloned volume
1365 1365
         # and clone status.
1366 1366
         model_update, cloned = self.driver.clone_image(
1367
-            volume_ref, image_location, image_id)
1367
+            volume_ref, image_location, image_id, image_meta)
1368 1368
         if not cloned:
1369 1369
             # TODO(harlowja): what needs to be rolled back in the clone if this
1370 1370
             # volume create fails?? Likely this should be a subflow or broken

Loading…
Cancel
Save