From e5abf57fe985fd0e837e3d92c0087dfbe13ad56c Mon Sep 17 00:00:00 2001 From: wanghao Date: Tue, 21 Mar 2017 17:59:51 +0800 Subject: [PATCH] Introduce managing existing snapshot to rbd driver Now Ceph can support to rename snapshot name, so managing existing snapshot will be introduced. Change-Id: Ia65c3d1de5a30251ec83a8ec6a8c381ba68e5c9f Implements: bp driver-rbd-to-support-managing-existing-snapshot --- cinder/tests/unit/volume/drivers/test_rbd.py | 94 +++++++++++++++++++ cinder/volume/drivers/rbd.py | 68 ++++++++++++++ ...ng-existing-snapshot-fb871a3ea98dc572.yaml | 3 + 3 files changed, 165 insertions(+) create mode 100644 releasenotes/notes/rbd-support-managing-existing-snapshot-fb871a3ea98dc572.yaml diff --git a/cinder/tests/unit/volume/drivers/test_rbd.py b/cinder/tests/unit/volume/drivers/test_rbd.py index 037bf0a4a40..a5cae1f4fe3 100644 --- a/cinder/tests/unit/volume/drivers/test_rbd.py +++ b/cinder/tests/unit/volume/drivers/test_rbd.py @@ -187,6 +187,16 @@ class RBDTestCase(test.TestCase): self.snapshot = fake_snapshot.fake_snapshot_obj( self.context, name='snapshot-0000000a') + self.snapshot_b = fake_snapshot.fake_snapshot_obj( + self.context, + **{'name': u'snapshot-0000000n', + 'expected_attrs': ['volume'], + 'volume': {'id': fake.VOLUME_ID, + 'name': 'cinder-volume', + 'size': 128, + 'host': 'host@fakebackend#fakepool'} + }) + @ddt.data({'cluster_name': None, 'pool_name': 'rbd'}, {'cluster_name': 'volumes', 'pool_name': None}) @ddt.unpack @@ -1608,6 +1618,90 @@ class RBDTestCase(test.TestCase): mock_exec.assert_called_once_with(self.volume_a.name, remote, 'mirror_image_promote', False) + @common_mocks + def test_manage_existing_snapshot_get_size(self): + with mock.patch.object(self.driver.rbd.Image(), 'size') as \ + mock_rbd_image_size: + with mock.patch.object(self.driver.rbd.Image(), 'close') \ + as mock_rbd_image_close: + mock_rbd_image_size.return_value = 2 * units.Gi + existing_ref = {'source-name': self.snapshot_b.name} + return_size = self.driver.manage_existing_snapshot_get_size( + self.snapshot_b, + existing_ref) + self.assertEqual(2, return_size) + mock_rbd_image_size.assert_called_once_with() + mock_rbd_image_close.assert_called_once_with() + + @common_mocks + def test_manage_existing_snapshot_get_non_integer_size(self): + rbd_snapshot = self.driver.rbd.Image.return_value + rbd_snapshot.size.return_value = int(1.75 * units.Gi) + existing_ref = {'source-name': self.snapshot_b.name} + return_size = self.driver.manage_existing_snapshot_get_size( + self.snapshot_b, existing_ref) + self.assertEqual(2, return_size) + rbd_snapshot.size.assert_called_once_with() + rbd_snapshot.close.assert_called_once_with() + + @common_mocks + def test_manage_existing_snapshot_get_invalid_size(self): + + with mock.patch.object(self.driver.rbd.Image(), 'size') as \ + mock_rbd_image_size: + with mock.patch.object(self.driver.rbd.Image(), 'close') \ + as mock_rbd_image_close: + mock_rbd_image_size.return_value = 'abcd' + existing_ref = {'source-name': self.snapshot_b.name} + self.assertRaises( + exception.VolumeBackendAPIException, + self.driver.manage_existing_snapshot_get_size, + self.snapshot_b, existing_ref) + + mock_rbd_image_size.assert_called_once_with() + mock_rbd_image_close.assert_called_once_with() + + @common_mocks + def test_manage_existing_snapshot_with_invalid_rbd_image(self): + self.mock_rbd.Image.side_effect = self.mock_rbd.ImageNotFound + + invalid_snapshot = 'snapshot-invalid' + invalid_ref = {'source-name': invalid_snapshot} + + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_snapshot_get_size, + self.snapshot_b, invalid_ref) + # Make sure the exception was raised + self.assertEqual([self.mock_rbd.ImageNotFound], + RAISED_EXCEPTIONS) + + @common_mocks + def test_manage_existing_snapshot(self): + proxy = self.mock_proxy.return_value + proxy.__enter__.return_value = proxy + exist_snapshot = 'snapshot-exist' + existing_ref = {'source-name': exist_snapshot} + proxy.rename_snap.return_value = 0 + self.driver.manage_existing_snapshot(self.snapshot_b, existing_ref) + proxy.rename_snap.assert_called_with(exist_snapshot, + self.snapshot_b.name) + + @common_mocks + def test_manage_existing_snapshot_with_exist_rbd_image(self): + proxy = self.mock_proxy.return_value + proxy.__enter__.return_value = proxy + proxy.rename_snap.side_effect = MockImageExistsException + + exist_snapshot = 'snapshot-exist' + existing_ref = {'source-name': exist_snapshot} + self.assertRaises(self.mock_rbd.ImageExists, + self.driver.manage_existing_snapshot, + self.snapshot_b, existing_ref) + + # Make sure the exception was raised + self.assertEqual(RAISED_EXCEPTIONS, + [self.mock_rbd.ImageExists]) + class ManagedRBDTestCase(test_driver.BaseDriverTestCase): driver_name = "cinder.volume.drivers.rbd.RBDDriver" diff --git a/cinder/volume/drivers/rbd.py b/cinder/volume/drivers/rbd.py index 92f6b192bfc..1eab302ea46 100644 --- a/cinder/volume/drivers/rbd.py +++ b/cinder/volume/drivers/rbd.py @@ -1322,3 +1322,71 @@ class RBDDriver(driver.CloneableImageVD, def migrate_volume(self, context, volume, host): return (False, None) + + def manage_existing_snapshot_get_size(self, snapshot, existing_ref): + """Return size of an existing image for manage_existing. + + :param snapshot: + snapshot ref info to be set + :param existing_ref: + existing_ref is a dictionary of the form: + {'source-name': } + """ + # Check that the reference is valid + if not isinstance(existing_ref, dict): + existing_ref = {"source-name": existing_ref} + if 'source-name' not in existing_ref: + reason = _('Reference must contain source-name element.') + raise exception.ManageExistingInvalidReference( + existing_ref=existing_ref, reason=reason) + + volume_name = utils.convert_str(snapshot.volume_name) + snapshot_name = utils.convert_str(existing_ref['source-name']) + + with RADOSClient(self) as client: + # Raise an exception if we didn't find a suitable rbd image. + try: + rbd_snapshot = self.rbd.Image(client.ioctx, volume_name, + snapshot=snapshot_name) + except self.rbd.ImageNotFound: + kwargs = {'existing_ref': snapshot_name, + 'reason': 'Specified snapshot does not exist.'} + raise exception.ManageExistingInvalidReference(**kwargs) + + snapshot_size = rbd_snapshot.size() + rbd_snapshot.close() + + # RBD image size is returned in bytes. Attempt to parse + # size as a float and round up to the next integer. + try: + convert_size = int(math.ceil(float(snapshot_size) / units.Gi)) + return convert_size + except ValueError: + exception_message = (_("Failed to manage existing snapshot " + "%(name)s, because reported size " + "%(size)s was not a floating-point" + " number.") + % {'name': snapshot_name, + 'size': snapshot_size}) + raise exception.VolumeBackendAPIException( + data=exception_message) + + def manage_existing_snapshot(self, snapshot, existing_ref): + """Manages an existing snapshot. + + Renames the snapshot name to match the expected name for the snapshot. + Error checking done by manage_existing_get_size is not repeated. + + :param snapshot: + snapshot ref info to be set + :param existing_ref: + existing_ref is a dictionary of the form: + {'source-name': } + """ + if not isinstance(existing_ref, dict): + existing_ref = {"source-name": existing_ref} + volume_name = utils.convert_str(snapshot.volume_name) + with RBDVolumeProxy(self, volume_name) as volume: + snapshot_name = existing_ref['source-name'] + volume.rename_snap(utils.convert_str(snapshot_name), + utils.convert_str(snapshot.name)) diff --git a/releasenotes/notes/rbd-support-managing-existing-snapshot-fb871a3ea98dc572.yaml b/releasenotes/notes/rbd-support-managing-existing-snapshot-fb871a3ea98dc572.yaml new file mode 100644 index 00000000000..2781af36444 --- /dev/null +++ b/releasenotes/notes/rbd-support-managing-existing-snapshot-fb871a3ea98dc572.yaml @@ -0,0 +1,3 @@ +--- +features: + - Allow rbd driver to manage existing snapshot.