From 23e7943b1df4d5c567db69ee44e85242f8aff7a4 Mon Sep 17 00:00:00 2001 From: YAMADA Hideki Date: Tue, 10 Mar 2015 05:13:02 +0000 Subject: [PATCH] Add backup/restore methods to Sheepdog driver The backup tests fail when running tempest. The Sheepdog driver has not implemented backup/restore methods yet. Co-Authored-By: Teruaki Ishizaki Change-Id: I8f6a82b046cfd1268092cc79111b9faedafe3c8b Closes-Bug: #1426440 Related-Bug: #1444899 --- cinder/tests/unit/test_sheepdog.py | 201 +++++++++++++++++++++++++++++ cinder/volume/drivers/sheepdog.py | 131 ++++++++++++++++++- 2 files changed, 330 insertions(+), 2 deletions(-) diff --git a/cinder/tests/unit/test_sheepdog.py b/cinder/tests/unit/test_sheepdog.py index b0e138911..09f52a779 100644 --- a/cinder/tests/unit/test_sheepdog.py +++ b/cinder/tests/unit/test_sheepdog.py @@ -23,6 +23,8 @@ from oslo_utils import importutils from oslo_utils import units import six +from cinder.backup import driver as backup_driver +from cinder import db from cinder import exception from cinder.image import image_utils from cinder import test @@ -59,6 +61,105 @@ class FakeImageService(object): pass +class SheepdogIOWrapperTestCase(test.TestCase): + def setUp(self): + super(SheepdogIOWrapperTestCase, self).setUp() + self.volume = {'name': 'volume-2f9b2ff5-987b-4412-a91c-23caaf0d5aff'} + self.snapshot_name = 'snapshot-bf452d80-068a-43d7-ba9f-196cf47bd0be' + + self.vdi_wrapper = sheepdog.SheepdogIOWrapper( + self.volume) + self.snapshot_wrapper = sheepdog.SheepdogIOWrapper( + self.volume, self.snapshot_name) + + self.execute = mock.MagicMock() + self.mock_object(processutils, 'execute', self.execute) + + def test_init(self): + self.assertEqual(self.volume['name'], self.vdi_wrapper._vdiname) + self.assertIsNone(self.vdi_wrapper._snapshot_name) + self.assertEqual(0, self.vdi_wrapper._offset) + + self.assertEqual(self.snapshot_name, + self.snapshot_wrapper._snapshot_name) + + def test_execute(self): + cmd = ('cmd1', 'arg1') + data = 'data1' + + self.vdi_wrapper._execute(cmd, data) + + self.execute.assert_called_once_with(*cmd, process_input=data) + + def test_execute_error(self): + cmd = ('cmd1', 'arg1') + data = 'data1' + self.mock_object(processutils, 'execute', + mock.MagicMock(side_effect=OSError)) + + args = (cmd, data) + self.assertRaises(exception.VolumeDriverException, + self.vdi_wrapper._execute, + *args) + + def test_read_vdi(self): + self.vdi_wrapper.read() + self.execute.assert_called_once_with( + 'dog', 'vdi', 'read', self.volume['name'], 0, process_input=None) + + def test_read_vdi_invalid(self): + self.vdi_wrapper._valid = False + self.assertRaises(exception.VolumeDriverException, + self.vdi_wrapper.read) + + def test_write_vdi(self): + data = 'data1' + + self.vdi_wrapper.write(data) + + self.execute.assert_called_once_with( + 'dog', 'vdi', 'write', + self.volume['name'], 0, len(data), + process_input=data) + self.assertEqual(len(data), self.vdi_wrapper.tell()) + + def test_write_vdi_invalid(self): + self.vdi_wrapper._valid = False + self.assertRaises(exception.VolumeDriverException, + self.vdi_wrapper.write, 'dummy_data') + + def test_read_snapshot(self): + self.snapshot_wrapper.read() + self.execute.assert_called_once_with( + 'dog', 'vdi', 'read', '-s', self.snapshot_name, + self.volume['name'], 0, + process_input=None) + + def test_seek(self): + self.vdi_wrapper.seek(12345) + self.assertEqual(12345, self.vdi_wrapper.tell()) + + self.vdi_wrapper.seek(-2345, whence=1) + self.assertEqual(10000, self.vdi_wrapper.tell()) + + # This results in negative offset. + self.assertRaises(IOError, self.vdi_wrapper.seek, -20000, whence=1) + + def test_seek_invalid(self): + seek_num = 12345 + self.vdi_wrapper._valid = False + self.assertRaises(exception.VolumeDriverException, + self.vdi_wrapper.seek, seek_num) + + def test_flush(self): + # flush does nothing. + self.vdi_wrapper.flush() + self.assertFalse(self.execute.called) + + def test_fileno(self): + self.assertRaises(IOError, self.vdi_wrapper.fileno) + + class SheepdogTestCase(test.TestCase): def setUp(self): super(SheepdogTestCase, self).setUp() @@ -336,3 +437,103 @@ class SheepdogTestCase(test.TestCase): "sheepdog:%s" % fake_vol['name'], "%sG" % fake_vol['size']] mock_exe.assert_called_once_with(*args) + + @mock.patch.object(db, 'volume_get') + @mock.patch.object(sheepdog.SheepdogDriver, '_try_execute') + @mock.patch.object(sheepdog.SheepdogDriver, 'create_snapshot') + @mock.patch.object(backup_driver, 'BackupDriver') + @mock.patch.object(sheepdog.SheepdogDriver, 'delete_snapshot') + def test_backup_volume_success(self, fake_delete_snapshot, + fake_backup_service, fake_create_snapshot, + fake_execute, fake_volume_get): + fake_context = {} + fake_backup = {'volume_id': '2926efe0-24ab-45b7-95e1-ff66e0646a33'} + fake_volume = {'id': '2926efe0-24ab-45b7-95e1-ff66e0646a33', + 'name': 'volume-2926efe0-24ab-45b7-95e1-ff66e0646a33'} + fake_volume_get.return_value = fake_volume + self.driver.backup_volume(fake_context, + fake_backup, + fake_backup_service) + + self.assertEqual(1, fake_create_snapshot.call_count) + self.assertEqual(2, fake_delete_snapshot.call_count) + self.assertEqual(fake_create_snapshot.call_args, + fake_delete_snapshot.call_args) + + call_args, call_kwargs = fake_backup_service.backup.call_args + call_backup, call_sheepdog_fd = call_args + self.assertEqual(fake_backup, call_backup) + self.assertIsInstance(call_sheepdog_fd, sheepdog.SheepdogIOWrapper) + + @mock.patch.object(db, 'volume_get') + @mock.patch.object(sheepdog.SheepdogDriver, '_try_execute') + @mock.patch.object(sheepdog.SheepdogDriver, 'create_snapshot') + @mock.patch.object(backup_driver, 'BackupDriver') + @mock.patch.object(sheepdog.SheepdogDriver, 'delete_snapshot') + def test_backup_volume_fail_to_create_snap(self, fake_delete_snapshot, + fake_backup_service, + fake_create_snapshot, + fake_execute, fake_volume_get): + fake_context = {} + fake_backup = {'volume_id': '2926efe0-24ab-45b7-95e1-ff66e0646a33'} + fake_volume = {'id': '2926efe0-24ab-45b7-95e1-ff66e0646a33', + 'name': 'volume-2926efe0-24ab-45b7-95e1-ff66e0646a33'} + fake_volume_get.return_value = fake_volume + fake_create_snapshot.side_effect = processutils.ProcessExecutionError( + cmd='dummy', exit_code=1, stdout='dummy', stderr='dummy') + + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.backup_volume, + fake_context, + fake_backup, + fake_backup_service) + self.assertEqual(1, fake_create_snapshot.call_count) + self.assertEqual(1, fake_delete_snapshot.call_count) + self.assertEqual(fake_create_snapshot.call_args, + fake_delete_snapshot.call_args) + + @mock.patch.object(db, 'volume_get') + @mock.patch.object(sheepdog.SheepdogDriver, '_try_execute') + @mock.patch.object(sheepdog.SheepdogDriver, 'create_snapshot') + @mock.patch.object(backup_driver, 'BackupDriver') + @mock.patch.object(sheepdog.SheepdogDriver, 'delete_snapshot') + def test_backup_volume_fail_to_backup_vol(self, fake_delete_snapshot, + fake_backup_service, + fake_create_snapshot, + fake_execute, fake_volume_get): + fake_context = {} + fake_backup = {'volume_id': '2926efe0-24ab-45b7-95e1-ff66e0646a33'} + fake_volume = {'id': '2926efe0-24ab-45b7-95e1-ff66e0646a33', + 'name': 'volume-2926efe0-24ab-45b7-95e1-ff66e0646a33'} + fake_volume_get.return_value = fake_volume + + class BackupError(Exception): + pass + + fake_backup_service.backup.side_effect = BackupError() + + self.assertRaises(BackupError, + self.driver.backup_volume, + fake_context, + fake_backup, + fake_backup_service) + self.assertEqual(1, fake_create_snapshot.call_count) + self.assertEqual(2, fake_delete_snapshot.call_count) + self.assertEqual(fake_create_snapshot.call_args, + fake_delete_snapshot.call_args) + + @mock.patch.object(backup_driver, 'BackupDriver') + def test_restore_backup(self, fake_backup_service): + fake_context = {} + fake_backup = {} + fake_volume = {'id': '2926efe0-24ab-45b7-95e1-ff66e0646a33', + 'name': 'volume-2926efe0-24ab-45b7-95e1-ff66e0646a33'} + + self.driver.restore_backup( + fake_context, fake_backup, fake_volume, fake_backup_service) + + call_args, call_kwargs = fake_backup_service.restore.call_args + call_backup, call_volume_id, call_sheepdog_fd = call_args + self.assertEqual(fake_backup, call_backup) + self.assertEqual(fake_volume['id'], call_volume_id) + self.assertIsInstance(call_sheepdog_fd, sheepdog.SheepdogIOWrapper) diff --git a/cinder/volume/drivers/sheepdog.py b/cinder/volume/drivers/sheepdog.py index 43ec85d9d..0892009b5 100644 --- a/cinder/volume/drivers/sheepdog.py +++ b/cinder/volume/drivers/sheepdog.py @@ -18,6 +18,8 @@ SheepDog Volume Driver. """ +import eventlet +import io import re from oslo_concurrency import processutils @@ -38,6 +40,101 @@ CONF = cfg.CONF CONF.import_opt("image_conversion_dir", "cinder.image.image_utils") +class SheepdogIOWrapper(io.RawIOBase): + """File-like object with Sheepdog backend.""" + + def __init__(self, volume, snapshot_name=None): + self._vdiname = volume['name'] + self._snapshot_name = snapshot_name + self._offset = 0 + # SheepdogIOWrapper instance becomes invalid if a write error occurs. + self._valid = True + + def _execute(self, cmd, data=None): + try: + # NOTE(yamada-h): processutils.execute causes busy waiting + # under eventlet. + # To avoid wasting CPU resources, it should not be used for + # the command which takes long time to execute. + # For workaround, we replace a subprocess module with + # the original one while only executing a read/write command. + _processutils_subprocess = processutils.subprocess + processutils.subprocess = eventlet.patcher.original('subprocess') + return processutils.execute(*cmd, process_input=data)[0] + except (processutils.ProcessExecutionError, OSError): + self._valid = False + msg = _('Sheepdog I/O Error, command was: "%s".') % ' '.join(cmd) + raise exception.VolumeDriverException(message=msg) + finally: + processutils.subprocess = _processutils_subprocess + + def read(self, length=None): + if not self._valid: + msg = _('An error occurred while reading volume "%s".' + ) % self._vdiname + raise exception.VolumeDriverException(message=msg) + + cmd = ['dog', 'vdi', 'read'] + if self._snapshot_name: + cmd.extend(('-s', self._snapshot_name)) + cmd.extend((self._vdiname, self._offset)) + if length: + cmd.append(length) + data = self._execute(cmd) + self._offset += len(data) + return data + + def write(self, data): + if not self._valid: + msg = _('An error occurred while writing to volume "%s".' + ) % self._vdiname + raise exception.VolumeDriverException(message=msg) + + length = len(data) + cmd = ('dog', 'vdi', 'write', self._vdiname, self._offset, length) + self._execute(cmd, data) + self._offset += length + return length + + def seek(self, offset, whence=0): + if not self._valid: + msg = _('An error occured while seeking for volume "%s".' + ) % self._vdiname + raise exception.VolumeDriverException(message=msg) + + if whence == 0: + # SEEK_SET or 0 - start of the stream (the default); + # offset should be zero or positive + new_offset = offset + elif whence == 1: + # SEEK_CUR or 1 - current stream position; offset may be negative + new_offset = self._offset + offset + else: + # SEEK_END or 2 - end of the stream; offset is usually negative + # TODO(yamada-h): Support SEEK_END + raise IOError(_("Invalid argument - whence=%s not supported.") % + whence) + + if new_offset < 0: + raise IOError(_("Invalid argument - negative seek offset.")) + + self._offset = new_offset + + def tell(self): + return self._offset + + def flush(self): + pass + + def fileno(self): + """Sheepdog does not have support for fileno so we raise IOError. + + Raising IOError is recommended way to notify caller that interface is + not supported - see http://docs.python.org/2/library/io.html#io.IOBase + """ + raise IOError(_("fileno is not supported by SheepdogIOWrapper")) + + class SheepdogDriver(driver.VolumeDriver): """Executes commands relating to Sheepdog Volumes.""" @@ -287,8 +384,38 @@ class SheepdogDriver(driver.VolumeDriver): def backup_volume(self, context, backup, backup_service): """Create a new backup from an existing volume.""" - raise NotImplementedError() + volume = self.db.volume_get(context, backup['volume_id']) + temp_snapshot = {'volume_name': volume['name'], + 'name': 'tmp-snap-%s' % volume['name']} + + # NOTE(tishizaki): If previous backup_volume operation has failed, + # a temporary snapshot for previous operation may exist. + # So, the old snapshot must be deleted before backup_volume. + # Sheepdog 0.9 or later 'delete_snapshot' operation + # is done successfully, although target snapshot does not exist. + # However, sheepdog 0.8 or before 'delete_snapshot' operation + # is failed, and raise ProcessExecutionError when target snapshot + # does not exist. + try: + self.delete_snapshot(temp_snapshot) + except (processutils.ProcessExecutionError): + pass + + try: + self.create_snapshot(temp_snapshot) + except (processutils.ProcessExecutionError, OSError): + msg = (_('Failed to create a temporary snapshot for volume %s.') + % volume['id']) + LOG.exception(msg) + raise exception.VolumeBackendAPIException(data=msg) + + try: + sheepdog_fd = SheepdogIOWrapper(volume, temp_snapshot['name']) + backup_service.backup(backup, sheepdog_fd) + finally: + self.delete_snapshot(temp_snapshot) def restore_backup(self, context, backup, volume, backup_service): """Restore an existing backup to a new or existing volume.""" - raise NotImplementedError() + sheepdog_fd = SheepdogIOWrapper(volume) + backup_service.restore(backup, volume['id'], sheepdog_fd) -- 2.45.2