From c9be1e4fa28c1d654b130d836c42db316d6a064d Mon Sep 17 00:00:00 2001 From: Ray Chen Date: Thu, 10 Jul 2014 23:01:17 +0800 Subject: [PATCH] Fix missing clone_image API support for sheepdog driver. If image is backend by sheepdog, it will use create_cloned_volume function to help to clone this image. Normally sheepdog can clone only from snapshot. This patch set is derived from abandoned change: Ibd355082e7c3215c00f6576404566577f7a2601c Change-Id: I6f6834ff5443f2d1a21d4ae10e26e1cab3664a01 Closes-Bug: #1140177 --- cinder/tests/test_sheepdog.py | 130 +++++++++++++++++++++++++++++- cinder/volume/drivers/sheepdog.py | 74 ++++++++++++++++- 2 files changed, 202 insertions(+), 2 deletions(-) diff --git a/cinder/tests/test_sheepdog.py b/cinder/tests/test_sheepdog.py index 70fa065bb48..58267edeb11 100644 --- a/cinder/tests/test_sheepdog.py +++ b/cinder/tests/test_sheepdog.py @@ -19,10 +19,14 @@ import contextlib import mock from oslo_concurrency import processutils +from oslo_utils import importutils from oslo_utils import units +import six +from cinder import exception from cinder.image import image_utils from cinder import test +from cinder.volume import configuration as conf from cinder.volume.drivers import sheepdog @@ -58,7 +62,13 @@ class FakeImageService: class SheepdogTestCase(test.TestCase): def setUp(self): super(SheepdogTestCase, self).setUp() - self.driver = sheepdog.SheepdogDriver() + self.driver = sheepdog.SheepdogDriver( + configuration=conf.Configuration(None)) + + db_driver = self.driver.configuration.db_driver + self.db = importutils.import_module(db_driver) + self.driver.db = self.db + self.driver.do_setup(None) def test_update_volume_stats(self): def fake_stats(*args): @@ -127,6 +137,124 @@ class SheepdogTestCase(test.TestCase): 'size': 1}, FakeImageService(), None) + def test_create_cloned_volume(self): + src_vol = { + 'project_id': 'testprjid', + 'name': six.text_type('volume-00000001'), + 'size': '20', + 'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66', + } + target_vol = { + 'project_id': 'testprjid', + 'name': six.text_type('volume-00000002'), + 'size': '20', + 'id': '582a1efa-be6a-11e4-a73b-0aa186c60fe0', + } + + with mock.patch.object(self.driver, + '_try_execute') as mock_exe: + self.driver.create_cloned_volume(target_vol, src_vol) + + snapshot_name = src_vol['name'] + '-temp-snapshot' + qemu_src_volume_name = "sheepdog:%s" % src_vol['name'] + qemu_snapshot_name = '%s:%s' % (qemu_src_volume_name, + snapshot_name) + qemu_target_volume_name = "sheepdog:%s" % target_vol['name'] + calls = [ + mock.call('qemu-img', 'snapshot', '-c', + snapshot_name, qemu_src_volume_name), + mock.call('qemu-img', 'create', '-b', + qemu_snapshot_name, + qemu_target_volume_name, + '%sG' % target_vol['size']), + ] + mock_exe.assert_has_calls(calls) + + def test_create_cloned_volume_failure(self): + fake_name = six.text_type('volume-00000001') + fake_size = '20' + fake_vol = {'project_id': 'testprjid', 'name': fake_name, + 'size': fake_size, + 'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66'} + src_vol = fake_vol + + patch = mock.patch.object + with patch(self.driver, '_try_execute', + side_effect=processutils.ProcessExecutionError): + with patch(self.driver, 'create_snapshot'): + with patch(self.driver, 'delete_snapshot'): + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_cloned_volume, + fake_vol, + src_vol) + + def test_clone_image_success(self): + context = {} + fake_name = six.text_type('volume-00000001') + fake_size = '2' + fake_vol = {'project_id': 'testprjid', 'name': fake_name, + 'size': fake_size, + 'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66'} + + image_location = ('sheepdog:192.168.1.111:7000:Alice', None) + image_id = "caa4ffd0-fake-fake-fake-f8631a807f5a" + image_meta = {'id': image_id, 'size': 1, 'disk_format': 'raw'} + image_service = '' + + patch = mock.patch.object + with patch(self.driver, '_try_execute', return_value=True): + with patch(self.driver, 'create_cloned_volume'): + with patch(self.driver, '_resize'): + model_updated, cloned = self.driver.clone_image( + context, fake_vol, image_location, + image_meta, image_service) + + self.assertTrue(cloned) + self.assertEqual("sheepdog:%s" % fake_name, + model_updated['provider_location']) + + def test_clone_image_failure(self): + context = {} + fake_vol = {} + image_location = ('image_location', None) + image_meta = {} + image_service = '' + + with mock.patch.object(self.driver, '_is_cloneable', + lambda *args: False): + result = self.driver.clone_image( + context, fake_vol, image_location, image_meta, image_service) + self.assertEqual(({}, False), result) + + def test_is_cloneable(self): + uuid = '87f1b01c-f46c-4537-bd5d-23962f5f4316' + location = 'sheepdog:ip:port:%s' % uuid + image_meta = {'id': uuid, 'size': 1, 'disk_format': 'raw'} + invalid_image_meta = {'id': uuid, 'size': 1, 'disk_format': 'iso'} + + with mock.patch.object(self.driver, '_try_execute') as try_execute: + self.assertTrue( + self.driver._is_cloneable(location, image_meta)) + expected_cmd = ('collie', 'vdi', 'list', + '--address', 'ip', + '--port', 'port', + uuid) + try_execute.assert_called_once_with(*expected_cmd) + + # check returning False without executing a command + self.assertFalse( + self.driver._is_cloneable('invalid-location', image_meta)) + self.assertFalse( + self.driver._is_cloneable(location, invalid_image_meta)) + self.assertEqual(1, try_execute.call_count) + + error = processutils.ProcessExecutionError + with mock.patch.object(self.driver, '_try_execute', + side_effect=error) as fail_try_execute: + self.assertFalse( + self.driver._is_cloneable(location, image_meta)) + fail_try_execute.assert_called_once_with(*expected_cmd) + def test_extend_volume(self): fake_name = u'volume-00000001' fake_size = '20' diff --git a/cinder/volume/drivers/sheepdog.py b/cinder/volume/drivers/sheepdog.py index 21d46f9b104..4bacc426178 100644 --- a/cinder/volume/drivers/sheepdog.py +++ b/cinder/volume/drivers/sheepdog.py @@ -63,8 +63,80 @@ class SheepdogDriver(driver.VolumeDriver): exception_message = _("Sheepdog is not working") raise exception.VolumeBackendAPIException(data=exception_message) + def _is_cloneable(self, image_location, image_meta): + """Check the image can be clone or not.""" + + if image_location is None: + return False + + if not image_location.startswith("sheepdog:"): + LOG.debug("Image is not stored in sheepdog.") + return False + + if image_meta['disk_format'] != 'raw': + LOG.debug("Image clone requires image format to be " + "'raw' but image %s(%s) is '%s'.", + image_location, + image_meta['id'], + image_meta['disk_format']) + return False + + cloneable = False + # check whether volume is stored in sheepdog + try: + # The image location would be like + # "sheepdog:192.168.10.2:7000:Alice" + (label, ip, port, name) = image_location.split(":", 3) + + self._try_execute('collie', 'vdi', 'list', '--address', ip, + '--port', port, name) + cloneable = True + except processutils.ProcessExecutionError as e: + LOG.debug("Can not find vdi %(image)s: %(err)s", + {'image': name, 'err': e}) + + return cloneable + + def clone_image(self, context, volume, + image_location, image_meta, + image_service): + """Create a volume efficiently from an existing image.""" + image_location = image_location[0] if image_location else None + if not self._is_cloneable(image_location, image_meta): + return {}, False + + # The image location would be like + # "sheepdog:192.168.10.2:7000:Alice" + (label, ip, port, name) = image_location.split(":", 3) + volume_ref = {'name': name, 'size': image_meta['size']} + self.create_cloned_volume(volume, volume_ref) + self._resize(volume) + + vol_path = self.local_path(volume) + return {'provider_location': vol_path}, True + def create_cloned_volume(self, volume, src_vref): - raise NotImplementedError() + """Clone a sheepdog volume from another volume.""" + + snapshot_name = src_vref['name'] + '-temp-snapshot' + snapshot = { + 'name': snapshot_name, + 'volume_name': src_vref['name'], + 'volume_size': src_vref['size'], + } + + self.create_snapshot(snapshot) + + try: + # Create volume + self.create_volume_from_snapshot(volume, snapshot) + except processutils.ProcessExecutionError: + msg = _('Failed to create cloned volume %s.') % volume['id'] + LOG.error(msg) + raise exception.VolumeBackendAPIException(msg) + finally: + # Delete temp Snapshot + self.delete_snapshot(snapshot) def create_volume(self, volume): """Create a sheepdog volume."""