]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add backup/restore methods to Sheepdog driver
authorYAMADA Hideki <yamada.hideki@lab.ntt.co.jp>
Tue, 10 Mar 2015 05:13:02 +0000 (05:13 +0000)
committerYAMADA Hideki <yamada.hideki@lab.ntt.co.jp>
Mon, 17 Aug 2015 08:05:15 +0000 (08:05 +0000)
The backup tests fail when running tempest.
The Sheepdog driver has not implemented backup/restore methods yet.

Co-Authored-By: Teruaki Ishizaki <ishizaki.teruaki@lab.ntt.co.jp>
Change-Id: I8f6a82b046cfd1268092cc79111b9faedafe3c8b
Closes-Bug: #1426440
Related-Bug: #1444899

cinder/tests/unit/test_sheepdog.py
cinder/volume/drivers/sheepdog.py

index b0e138911a2b84431c93abe8b282156a74356f9d..09f52a779409aa7a0ac2976f4a0cbd623f034b1c 100644 (file)
@@ -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)
index 43ec85d9d515e3463c852600dcf0977c4aaa030d..0892009b5c619cfa0d5db33035fc9af165aa411f 100644 (file)
@@ -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)