-# Copyright (c) 2013 Scality
+# Copyright (c) 2015 Scality
+# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
"""
Unit tests for the Scality SOFS Volume Driver.
"""
-
import errno
import os
-import shutil
-import tempfile
import mock
-from mox3 import mox as mox_lib
-from oslo_utils import units
+from six.moves import urllib
from cinder import context
from cinder import exception
-from cinder.image import image_utils
+from cinder.openstack.common import imageutils
from cinder import test
from cinder.volume import configuration as conf
-from cinder.volume.drivers import scality
+import cinder.volume.drivers.scality as driver
+
+_FAKE_VOLUME = {'name': 'volume-a79d463e-1fd5-11e5-a6ff-5b81bfee8544',
+ 'id': 'a79d463e-1fd5-11e5-a6ff-5b81bfee8544',
+ 'provider_location': 'fake_share'}
+_FAKE_SNAPSHOT = {'id': 'ae3d6da2-1fd5-11e5-967f-1b8cf3b401ab',
+ 'volume': _FAKE_VOLUME,
+ 'status': 'available',
+ 'provider_location': None,
+ 'volume_size': 1,
+ 'name': 'snapshot-ae3d6da2-1fd5-11e5-967f-1b8cf3b401ab'}
+_FAKE_BACKUP = {'id': '914849d2-2585-11e5-be54-d70ca0c343d6',
+ 'volume_id': _FAKE_VOLUME['id']}
+
+_FAKE_MNT_POINT = '/tmp'
+_FAKE_SOFS_CONFIG = '/etc/sfused.conf'
+_FAKE_VOLUME_DIR = 'cinder/volumes'
+_FAKE_VOL_BASEDIR = os.path.join(_FAKE_MNT_POINT, _FAKE_VOLUME_DIR, '00')
+_FAKE_VOL_PATH = os.path.join(_FAKE_VOL_BASEDIR, _FAKE_VOLUME['name'])
+_FAKE_SNAP_PATH = os.path.join(_FAKE_VOL_BASEDIR, _FAKE_SNAPSHOT['name'])
+
+_FAKE_MOUNTS_TABLE = [['tmpfs /dev/shm\n'],
+ ['fuse ' + _FAKE_MNT_POINT + '\n']]
class ScalityDriverTestCase(test.TestCase):
"""Test case for the Scality driver."""
- TEST_MOUNT = '/tmp/fake_mount'
- TEST_CONFIG = '/tmp/fake_config'
- TEST_VOLDIR = 'volumes'
-
- TEST_VOLNAME = 'volume_name'
- TEST_VOLSIZE = '1'
- TEST_VOLUME = {
- 'name': TEST_VOLNAME,
- 'size': TEST_VOLSIZE
- }
- TEST_VOLPATH = os.path.join(TEST_MOUNT,
- TEST_VOLDIR,
- TEST_VOLNAME)
-
- TEST_SNAPNAME = 'snapshot_name'
- TEST_SNAPSHOT = {
- 'name': TEST_SNAPNAME,
- 'volume_name': TEST_VOLNAME,
- 'volume_size': TEST_VOLSIZE
- }
- TEST_SNAPPATH = os.path.join(TEST_MOUNT,
- TEST_VOLDIR,
- TEST_SNAPNAME)
-
- TEST_CLONENAME = 'clone_name'
- TEST_CLONE = {
- 'name': TEST_CLONENAME,
- 'size': TEST_VOLSIZE
- }
-
- TEST_NEWSIZE = '2'
-
- TEST_IMAGE_SERVICE = 'image_service'
- TEST_IMAGE_ID = 'image_id'
- TEST_IMAGE_META = 'image_meta'
-
- def _makedirs(self, path):
- try:
- os.makedirs(path)
- except OSError as e:
- if e.errno != errno.EEXIST:
- raise
-
- def _create_fake_config(self):
- open(self.TEST_CONFIG, "w+").close()
-
- def _create_fake_mount(self):
- self._makedirs(os.path.join(self.TEST_MOUNT, self.TEST_VOLDIR))
-
- def _remove_fake_config(self):
- try:
- os.unlink(self.TEST_CONFIG)
- except OSError as e:
- if e.errno != errno.ENOENT:
- raise
-
- def _configure_driver(self):
- self.configuration.scality_sofs_config = self.TEST_CONFIG
- self.configuration.scality_sofs_mount_point = self.TEST_MOUNT
- self.configuration.scality_sofs_volume_dir = self.TEST_VOLDIR
- self.configuration.volume_dd_blocksize = '1M'
-
- def _set_access_wrapper(self, is_visible):
-
- def _access_wrapper(path, flags):
- if path == '/sbin/mount.sofs':
- return is_visible
- else:
- return os.access(path, flags)
-
- self.stubs.Set(os, 'access', _access_wrapper)
-
def setUp(self):
- self.tempdir = tempfile.mkdtemp()
- self.addCleanup(shutil.rmtree, self.tempdir)
-
- self.TEST_MOUNT = self.tempdir
- self.TEST_VOLPATH = os.path.join(self.TEST_MOUNT,
- self.TEST_VOLDIR,
- self.TEST_VOLNAME)
- self.TEST_SNAPPATH = os.path.join(self.TEST_MOUNT,
- self.TEST_VOLDIR,
- self.TEST_SNAPNAME)
- self.TEST_CLONEPATH = os.path.join(self.TEST_MOUNT,
- self.TEST_VOLDIR,
- self.TEST_CLONENAME)
-
- self.configuration = mox_lib.MockObject(conf.Configuration)
- self._configure_driver()
super(ScalityDriverTestCase, self).setUp()
- self._driver = scality.ScalityDriver(configuration=self.configuration)
- self._driver.set_execute(lambda *args, **kwargs: None)
- self._create_fake_mount()
- self._create_fake_config()
- self.addCleanup(self._remove_fake_config)
+ self.cfg = mock.Mock(spec=conf.Configuration)
+ self.cfg.scality_sofs_mount_point = _FAKE_MNT_POINT
+ self.cfg.scality_sofs_config = _FAKE_SOFS_CONFIG
+ self.cfg.scality_sofs_volume_dir = _FAKE_VOLUME_DIR
+
+ self.drv = driver.ScalityDriver(configuration=self.cfg)
+ self.drv.db = mock.Mock()
+
+ @mock.patch.object(driver.urllib.request, 'urlopen')
+ @mock.patch('os.access')
+ def test_check_for_setup_error(self, mock_os_access, mock_urlopen):
+ self.drv.check_for_setup_error()
+
+ mock_urlopen.assert_called_once_with('file://%s' % _FAKE_SOFS_CONFIG,
+ timeout=5)
+ mock_os_access.assert_called_once_with('/sbin/mount.sofs', os.X_OK)
+
+ def test_check_for_setup_error_with_no_sofs_config(self):
+ self.cfg.scality_sofs_config = ''
- def test_setup_no_config(self):
- """Missing SOFS configuration shall raise an error."""
- self.configuration.scality_sofs_config = None
+ self.drv = driver.ScalityDriver(configuration=self.cfg)
+
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.drv.check_for_setup_error)
+
+ @mock.patch.object(driver.urllib.request, 'urlopen')
+ def test_check_for_setup_error_with_urlerror(self, mock_urlopen):
+ # Add a Unicode char to be sure that the exception is properly
+ # handled even if it contains Unicode chars
+ mock_urlopen.side_effect = urllib.error.URLError(u'\u9535')
self.assertRaises(exception.VolumeBackendAPIException,
- self._driver.do_setup, None)
+ self.drv.check_for_setup_error)
- def test_setup_missing_config(self):
- """Non-existent SOFS configuration file shall raise an error."""
- self.configuration.scality_sofs_config = 'nonexistent.conf'
+ @mock.patch.object(driver.urllib.request, 'urlopen')
+ def test_check_for_setup_error_with_httperror(self, mock_urlopen):
+ mock_urlopen.side_effect = urllib.error.HTTPError(*[None] * 5)
self.assertRaises(exception.VolumeBackendAPIException,
- self._driver.do_setup, None)
+ self.drv.check_for_setup_error)
- def test_setup_no_mount_helper(self):
- """SOFS must be installed to use the driver."""
- self._set_access_wrapper(False)
+ @mock.patch.object(driver.urllib.request, 'urlopen', mock.Mock())
+ @mock.patch('os.access')
+ def test_check_for_setup_error_with_no_mountsofs(self, mock_os_access):
+ mock_os_access.return_value = False
self.assertRaises(exception.VolumeBackendAPIException,
- self._driver.do_setup, None)
-
- def test_setup_make_voldir(self):
- """The directory for volumes shall be created automatically."""
- self._set_access_wrapper(True)
- voldir_path = os.path.join(self.TEST_MOUNT, self.TEST_VOLDIR)
- os.rmdir(voldir_path)
- fake_mounts = [['tmpfs /dev/shm\n'],
- ['fuse ' + self.TEST_MOUNT + '\n']]
- with mock.patch.object(scality.volume_utils, 'read_proc_mounts',
- side_effect=fake_mounts) as mock_get_mounts:
- self._driver.do_setup(None)
- self.assertEqual(2, mock_get_mounts.call_count)
- self.assertTrue(os.path.isdir(voldir_path))
-
- def test_local_path(self):
- """Expected behaviour for local_path."""
- self.assertEqual(self.TEST_VOLPATH,
- self._driver.local_path(self.TEST_VOLUME))
-
- def test_create_volume(self):
- """Expected behaviour for create_volume."""
- ret = self._driver.create_volume(self.TEST_VOLUME)
- self.assertEqual(os.path.join(self.TEST_VOLDIR,
- self.TEST_VOLNAME),
- ret['provider_location'])
- self.assertTrue(os.path.isfile(self.TEST_VOLPATH))
- self.assertEqual(1 * units.Gi,
- os.stat(self.TEST_VOLPATH).st_size)
-
- def test_delete_volume(self):
- """Expected behaviour for delete_volume."""
- self._driver.create_volume(self.TEST_VOLUME)
- self._driver.delete_volume(self.TEST_VOLUME)
- self.assertFalse(os.path.isfile(self.TEST_VOLPATH))
-
- def test_create_snapshot(self):
- """Expected behaviour for create_snapshot."""
- mox = self.mox
-
- vol_size = self._driver._size_bytes(self.TEST_VOLSIZE)
-
- mox.StubOutWithMock(self._driver, '_create_file')
- self._driver._create_file(self.TEST_SNAPPATH, vol_size)
- mox.StubOutWithMock(self._driver, '_copy_file')
- self._driver._copy_file(self.TEST_VOLPATH, self.TEST_SNAPPATH)
-
- mox.ReplayAll()
-
- self._driver.create_snapshot(self.TEST_SNAPSHOT)
-
- def test_delete_snapshot(self):
- """Expected behaviour for delete_snapshot."""
- mox = self.mox
-
- mox.StubOutWithMock(os, 'remove')
- os.remove(self.TEST_SNAPPATH)
-
- mox.ReplayAll()
-
- self._driver.delete_snapshot(self.TEST_SNAPSHOT)
-
- def test_initialize_connection(self):
- """Expected behaviour for initialize_connection."""
- ret = self._driver.initialize_connection(self.TEST_VOLUME, None)
- self.assertEqual('scality', ret['driver_volume_type'])
- self.assertEqual(os.path.join(self.TEST_VOLDIR, self.TEST_VOLNAME),
- ret['data']['sofs_path'])
- self.assertEqual(self.TEST_VOLDIR, ret['data']['export'])
- self.assertEqual(self.TEST_VOLNAME, ret['data']['name'])
-
- def test_copy_image_to_volume(self):
- """Expected behaviour for copy_image_to_volume."""
- self.mox.StubOutWithMock(image_utils, 'fetch_to_raw')
-
- image_utils.fetch_to_raw(context,
- self.TEST_IMAGE_SERVICE,
- self.TEST_IMAGE_ID,
- self.TEST_VOLPATH,
- mox_lib.IgnoreArg(),
- size=self.TEST_VOLSIZE)
-
- self.mox.ReplayAll()
-
- self._driver.copy_image_to_volume(context,
- self.TEST_VOLUME,
- self.TEST_IMAGE_SERVICE,
- self.TEST_IMAGE_ID)
-
- def test_copy_volume_to_image(self):
- """Expected behaviour for copy_volume_to_image."""
- self.mox.StubOutWithMock(image_utils, 'upload_volume')
-
- image_utils.upload_volume(context,
- self.TEST_IMAGE_SERVICE,
- self.TEST_IMAGE_META,
- self.TEST_VOLPATH)
-
- self.mox.ReplayAll()
-
- self._driver.copy_volume_to_image(context,
- self.TEST_VOLUME,
- self.TEST_IMAGE_SERVICE,
- self.TEST_IMAGE_META)
-
- def test_create_cloned_volume(self):
- """Expected behaviour for create_cloned_volume."""
- self.mox.StubOutWithMock(self._driver, '_create_file')
- self.mox.StubOutWithMock(self._driver, '_copy_file')
-
- vol_size = self._driver._size_bytes(self.TEST_VOLSIZE)
- self._driver._create_file(self.TEST_CLONEPATH, vol_size)
- self._driver._copy_file(self.TEST_VOLPATH, self.TEST_CLONEPATH)
+ self.drv.check_for_setup_error)
+ mock_os_access.assert_called_once_with('/sbin/mount.sofs', os.X_OK)
+
+ def test_load_shares_config(self):
+ self.assertEqual({}, self.drv.shares)
+ self.drv._load_shares_config()
+ self.assertEqual({_FAKE_VOLUME_DIR: None}, self.drv.shares)
+
+ def test_get_mount_point_for_share(self):
+ self.assertEqual(_FAKE_VOL_BASEDIR,
+ self.drv._get_mount_point_for_share())
+
+ @mock.patch("cinder.volume.utils.read_proc_mounts")
+ @mock.patch("oslo_concurrency.processutils.execute")
+ def test_ensure_share_mounted_when_mount_failed(self, mock_execute,
+ mock_read_proc_mounts):
+ mock_read_proc_mounts.return_value = ['tmpfs /dev/shm\n']
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.drv._ensure_share_mounted)
+ self.assertEqual(2, mock_read_proc_mounts.call_count)
+ self.assertEqual(1, mock_execute.call_count)
+
+ @mock.patch("cinder.volume.utils.read_proc_mounts")
+ @mock.patch("oslo_concurrency.processutils.execute")
+ @mock.patch("oslo_utils.fileutils.ensure_tree")
+ @mock.patch("os.symlink")
+ def test_ensure_shares_mounted(self, mock_symlink, mock_ensure_tree,
+ mock_execute, mock_read_proc_mounts):
+ self.assertEqual([], self.drv._mounted_shares)
+
+ mock_read_proc_mounts.side_effect = _FAKE_MOUNTS_TABLE
+
+ self.drv._ensure_shares_mounted()
+
+ self.assertEqual([_FAKE_VOLUME_DIR], self.drv._mounted_shares)
+ self.assertEqual(2, mock_read_proc_mounts.call_count)
+ mock_symlink.assert_called_once_with('.', _FAKE_VOL_BASEDIR)
+ self.assertEqual(2, mock_ensure_tree.call_count)
+ self.assertEqual(1, mock_execute.call_count)
+ expected_args = ('mount', '-t', 'sofs', _FAKE_SOFS_CONFIG,
+ _FAKE_MNT_POINT)
+ self.assertEqual(expected_args, mock_execute.call_args[0])
+
+ def test_find_share_when_no_shares_mounted(self):
+ self.assertRaises(exception.RemoteFSNoSharesMounted,
+ self.drv._find_share, 'ignored')
+
+ @mock.patch("cinder.volume.utils.read_proc_mounts")
+ @mock.patch("oslo_concurrency.processutils.execute")
+ @mock.patch("oslo_utils.fileutils.ensure_tree")
+ @mock.patch("os.symlink")
+ def test_find_share(self, mock_symlink, mock_ensure_tree, mock_execute,
+ mock_read_proc_mounts):
+ mock_read_proc_mounts.side_effect = _FAKE_MOUNTS_TABLE
+
+ self.drv._ensure_shares_mounted()
+
+ self.assertEqual(_FAKE_VOLUME_DIR, self.drv._find_share('ignored'))
+ self.assertEqual(2, mock_read_proc_mounts.call_count)
+ self.assertEqual(1, mock_execute.call_count)
+
+ expected_args = ('mount', '-t', 'sofs', _FAKE_SOFS_CONFIG,
+ _FAKE_MNT_POINT)
+ self.assertEqual(expected_args, mock_execute.call_args[0])
+
+ mock_symlink.assert_called_once_with('.', _FAKE_VOL_BASEDIR)
+
+ self.assertEqual(mock_ensure_tree.call_args_list,
+ [mock.call(_FAKE_MNT_POINT),
+ mock.call(os.path.join(_FAKE_MNT_POINT,
+ _FAKE_VOLUME_DIR))])
+
+ def test_get_volume_stats(self):
+ with mock.patch.object(self.cfg, 'safe_get') as mock_safe_get:
+ mock_safe_get.return_value = 'fake_backend_name'
+ stats = self.drv.get_volume_stats()
+ self.assertEqual(self.drv.VERSION, stats['driver_version'])
+ self.assertEqual(mock_safe_get.return_value,
+ stats['volume_backend_name'])
+ mock_safe_get.assert_called_once_with('volume_backend_name')
+
+ @mock.patch("cinder.image.image_utils.qemu_img_info")
+ def test_initialize_connection(self, mock_qemu_img_info):
+ info = imageutils.QemuImgInfo()
+ info.file_format = 'raw'
+ info.image = _FAKE_VOLUME['name']
+ mock_qemu_img_info.return_value = info
+
+ with mock.patch.object(self.drv, 'get_active_image_from_info') as \
+ mock_get_active_image_from_info:
+
+ mock_get_active_image_from_info.return_value = _FAKE_VOLUME['name']
+ conn_info = self.drv.initialize_connection(_FAKE_VOLUME, None)
+
+ expected_conn_info = {
+ 'driver_volume_type': driver.ScalityDriver.driver_volume_type,
+ 'mount_point_base': _FAKE_MNT_POINT,
+ 'data': {
+ 'export': _FAKE_VOLUME['provider_location'],
+ 'name': _FAKE_VOLUME['name'],
+ 'sofs_path': 'cinder/volumes/00/' + _FAKE_VOLUME['name'],
+ 'format': 'raw'
+ }
+ }
+ self.assertEqual(expected_conn_info, conn_info)
+ mock_get_active_image_from_info.assert_called_once_with(_FAKE_VOLUME)
+ mock_qemu_img_info.assert_called_once_with(_FAKE_VOL_PATH)
+
+ @mock.patch("cinder.image.image_utils.resize_image")
+ @mock.patch("cinder.image.image_utils.qemu_img_info")
+ def test_extend_volume(self, mock_qemu_img_info, mock_resize_image):
+ info = imageutils.QemuImgInfo()
+ info.file_format = 'raw'
+ mock_qemu_img_info.return_value = info
+
+ self.drv.extend_volume(_FAKE_VOLUME, 2)
+
+ mock_qemu_img_info.assert_called_once_with(_FAKE_VOL_PATH)
+
+ mock_resize_image.assert_called_once_with(_FAKE_VOL_PATH, 2)
+
+ @mock.patch("cinder.image.image_utils.qemu_img_info")
+ def test_extend_volume_with_invalid_format(self, mock_qemu_img_info):
+ info = imageutils.QemuImgInfo()
+ info.file_format = 'vmdk'
+ mock_qemu_img_info.return_value = info
+
+ self.assertRaises(exception.InvalidVolume,
+ self.drv.extend_volume, _FAKE_VOLUME, 2)
+
+ @mock.patch("cinder.image.image_utils.resize_image")
+ @mock.patch("cinder.image.image_utils.convert_image")
+ def test_copy_volume_from_snapshot_with_ioerror(self, mock_convert_image,
+ mock_resize_image):
+ with mock.patch.object(self.drv, '_read_info_file') as \
+ mock_read_info_file, \
+ mock.patch.object(self.drv, '_set_rw_permissions_for_all') as \
+ mock_set_rw_permissions:
+ mock_read_info_file.side_effect = IOError(errno.ENOENT, '')
+ self.drv._copy_volume_from_snapshot(_FAKE_SNAPSHOT,
+ _FAKE_VOLUME, 1)
+
+ mock_read_info_file.assert_called_once_with("%s.info" % _FAKE_VOL_PATH)
+ mock_convert_image.assert_called_once_with(_FAKE_SNAP_PATH,
+ _FAKE_VOL_PATH, 'raw',
+ run_as_root=True)
+ mock_set_rw_permissions.assert_called_once_with(_FAKE_VOL_PATH)
+ mock_resize_image.assert_called_once_with(_FAKE_VOL_PATH, 1)
+
+ @mock.patch("cinder.image.image_utils.resize_image")
+ @mock.patch("cinder.image.image_utils.convert_image")
+ @mock.patch("cinder.image.image_utils.qemu_img_info")
+ def test_copy_volume_from_snapshot(self, mock_qemu_img_info,
+ mock_convert_image, mock_resize_image):
+
+ new_volume = {'name': 'volume-3fa63b02-1fe5-11e5-b492-abf97a8fb23b',
+ 'id': '3fa63b02-1fe5-11e5-b492-abf97a8fb23b',
+ 'provider_location': 'fake_share'}
+ new_vol_path = os.path.join(_FAKE_VOL_BASEDIR, new_volume['name'])
+
+ info = imageutils.QemuImgInfo()
+ info.file_format = 'raw'
+ info.backing_file = _FAKE_VOL_PATH
+ mock_qemu_img_info.return_value = info
+
+ with mock.patch.object(self.drv, '_read_info_file') as \
+ mock_read_info_file, \
+ mock.patch.object(self.drv, '_set_rw_permissions_for_all') as \
+ mock_set_rw_permissions:
+ self.drv._copy_volume_from_snapshot(_FAKE_SNAPSHOT,
+ new_volume, 1)
+
+ mock_read_info_file.assert_called_once_with("%s.info" % _FAKE_VOL_PATH)
+ mock_convert_image.assert_called_once_with(_FAKE_VOL_PATH,
+ new_vol_path, 'raw',
+ run_as_root=True)
+ mock_set_rw_permissions.assert_called_once_with(new_vol_path)
+ mock_resize_image.assert_called_once_with(new_vol_path, 1)
+
+ @mock.patch("cinder.image.image_utils.qemu_img_info")
+ @mock.patch("cinder.utils.temporary_chown")
+ @mock.patch("six.moves.builtins.open")
+ def test_backup_volume(self, mock_open, mock_temporary_chown,
+ mock_qemu_img_info):
+ """Backup a volume with no snapshots."""
+
+ info = imageutils.QemuImgInfo()
+ info.file_format = 'raw'
+ mock_qemu_img_info.return_value = info
+
+ backup = {'volume_id': _FAKE_VOLUME['id']}
+ mock_backup_service = mock.MagicMock()
+ self.drv.db.volume_get.return_value = _FAKE_VOLUME
+
+ self.drv.backup_volume(context, backup, mock_backup_service)
+
+ mock_qemu_img_info.assert_called_once_with(_FAKE_VOL_PATH)
+ mock_temporary_chown.assert_called_once_with(_FAKE_VOL_PATH)
+ mock_open.assert_called_once_with(_FAKE_VOL_PATH)
+ mock_backup_service.backup.assert_called_once_with(
+ backup, mock_open().__enter__())
+
+ @mock.patch("cinder.image.image_utils.qemu_img_info")
+ def test_backup_volume_with_non_raw_volume(self, mock_qemu_img_info):
- self.mox.ReplayAll()
-
- self._driver.create_cloned_volume(self.TEST_CLONE, self.TEST_VOLUME)
+ info = imageutils.QemuImgInfo()
+ info.file_format = 'qcow2'
+ mock_qemu_img_info.return_value = info
- def test_extend_volume(self):
- """Expected behaviour for extend_volume."""
- self.mox.StubOutWithMock(self._driver, '_create_file')
+ self.drv.db.volume_get.return_value = _FAKE_VOLUME
- new_size = self._driver._size_bytes(self.TEST_NEWSIZE)
- self._driver._create_file(self.TEST_VOLPATH, new_size)
+ self.assertRaises(exception.InvalidVolume, self.drv.backup_volume,
+ context, _FAKE_BACKUP, mock.MagicMock())
- self.mox.ReplayAll()
+ mock_qemu_img_info.assert_called_once_with(_FAKE_VOL_PATH)
- self._driver.extend_volume(self.TEST_VOLUME, self.TEST_NEWSIZE)
+ @mock.patch("cinder.image.image_utils.qemu_img_info")
+ def test_backup_volume_with_backing_file(self, mock_qemu_img_info):
- @mock.patch('six.moves.builtins.open')
- @mock.patch('cinder.utils.temporary_chown')
- @mock.patch('cinder.image.image_utils.qemu_img_info')
- def test_backup_volume(self, qemu_img_info, temporary_chown, mock_open):
- volume = {'id': '2', 'name': self.TEST_VOLNAME}
- backup = {'volume_id': volume['id']}
- info = mock.Mock()
+ info = imageutils.QemuImgInfo()
info.file_format = 'raw'
- info.backing_file = None
- qemu_img_info.return_value = info
- backup_service = mock.Mock()
-
- with mock.patch.object(self._driver, 'db') as db:
- db.volume_get.return_value = volume
-
- self._driver.backup_volume(context, backup, backup_service)
-
- db.volume_get.assert_called_once_with(context, volume['id'])
- qemu_img_info.assert_called_once_with(self.TEST_VOLPATH)
- temporary_chown.asser_called_once_with(self.TEST_VOLPATH)
- mock_open.assert_called_once_with(self.TEST_VOLPATH)
- backup_service.backup.asser_called_once_with(backup, mock.ANY)
-
- info.backing_file = 'not None'
- self.assertRaises(exception.InvalidVolume,
- self._driver.backup_volume,
- context, backup, backup_service)
-
- info.file_format = 'not raw'
- self.assertRaises(exception.InvalidVolume,
- self._driver.backup_volume,
- context, backup, backup_service)
-
- @mock.patch('six.moves.builtins.open')
- @mock.patch('cinder.utils.temporary_chown')
- def test_restore_backup(self, temporary_chown, mock_open):
- volume = {'id': '2', 'name': self.TEST_VOLNAME}
- backup = {'id': 123, 'volume_id': volume['id']}
- backup_service = mock.Mock()
-
- self._driver.restore_backup(context, backup, volume,
- backup_service)
-
- temporary_chown.asser_called_once_with(self.TEST_VOLPATH)
- mock_open.assert_called_once_with(self.TEST_VOLPATH, 'wb')
- backup_service.restore.asser_called_once_with(backup, volume['id'],
- mock.ANY)
+ info.backing_file = 'fake.img'
+ mock_qemu_img_info.return_value = info
+
+ backup = {'volume_id': _FAKE_VOLUME['id']}
+ self.drv.db.volume_get.return_value = _FAKE_VOLUME
+
+ self.assertRaises(exception.InvalidVolume, self.drv.backup_volume,
+ context, backup, mock.MagicMock())
+
+ mock_qemu_img_info.assert_called_once_with(_FAKE_VOL_PATH)
+
+ @mock.patch("cinder.utils.temporary_chown")
+ @mock.patch("six.moves.builtins.open")
+ def test_restore_bakup(self, mock_open, mock_temporary_chown):
+ mock_backup_service = mock.MagicMock()
+
+ self.drv.restore_backup(context, _FAKE_BACKUP, _FAKE_VOLUME,
+ mock_backup_service)
+
+ mock_temporary_chown.assert_called_once_with(_FAKE_VOL_PATH)
+ mock_open.assert_called_once_with(_FAKE_VOL_PATH, 'wb')
+ mock_backup_service.restore.assert_called_once_with(
+ _FAKE_BACKUP, _FAKE_VOLUME['id'], mock_open().__enter__())
-# Copyright (c) 2013 Scality
+# Copyright (c) 2015 Scality
+# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
"""
+import errno
import os
from oslo_concurrency import lockutils
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import fileutils
-from oslo_utils import units
+import six
from six.moves import urllib
from cinder import exception
from cinder.i18n import _, _LI
from cinder.image import image_utils
from cinder import utils
-from cinder.volume import driver
+from cinder.volume.drivers import remotefs as remotefs_drv
from cinder.volume import utils as volume_utils
CONF.register_opts(volume_opts)
-class ScalityDriver(driver.VolumeDriver):
+class ScalityDriver(remotefs_drv.RemoteFSSnapDriver):
"""Scality SOFS cinder driver.
Creates sparse files on SOFS for hypervisors to use as block
devices.
"""
- VERSION = '1.0.0'
+ driver_volume_type = 'scality'
+ driver_prefix = 'scality_sofs'
+ volume_backend_name = 'Scality_SOFS'
+ VERSION = '2.0.0'
def __init__(self, *args, **kwargs):
super(ScalityDriver, self).__init__(*args, **kwargs)
self.configuration.append_config_values(volume_opts)
- def _check_prerequisites(self):
+ self.sofs_mount_point = self.configuration.scality_sofs_mount_point
+ self.sofs_config = self.configuration.scality_sofs_config
+ self.sofs_rel_volume_dir = self.configuration.scality_sofs_volume_dir
+ self.sofs_abs_volume_dir = os.path.join(self.sofs_mount_point,
+ self.sofs_rel_volume_dir)
+
+ # The following config flag is used by RemoteFSDriver._do_create_volume
+ # We want to use sparse file (ftruncated) without exposing this
+ # as a config switch to customers.
+ self.configuration.scality_sofs_sparsed_volumes = True
+
+ def check_for_setup_error(self):
"""Sanity checks before attempting to mount SOFS."""
# config is mandatory
- config = self.configuration.scality_sofs_config
- if not config:
+ if not self.sofs_config:
msg = _("Value required for 'scality_sofs_config'")
- LOG.warning(msg)
+ LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
# config can be a file path or a URL, check it
- if urllib.parse.urlparse(config).scheme == '':
+ config = self.sofs_config
+ if urllib.parse.urlparse(self.sofs_config).scheme == '':
# turn local path into URL
- config = 'file://%s' % config
+ config = 'file://%s' % self.sofs_config
try:
urllib.request.urlopen(config, timeout=5).close()
- except urllib.error.URLError as e:
- msg = _("Cannot access 'scality_sofs_config': %s") % e
- LOG.warning(msg)
+ except (urllib.error.URLError, urllib.error.HTTPError) as e:
+ msg = _("Can't access 'scality_sofs_config'"
+ ": %s") % six.text_type(e)
+ LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
# mount.sofs must be installed
if not os.access('/sbin/mount.sofs', os.X_OK):
msg = _("Cannot execute /sbin/mount.sofs")
- LOG.warning(msg)
+ LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
- @lockutils.synchronized('mount-sofs', 'cinder-sofs', external=True)
- def _mount_sofs(self):
- config = self.configuration.scality_sofs_config
- mount_path = self.configuration.scality_sofs_mount_point
+ def _load_shares_config(self, share_file=None):
+ self.shares[self.sofs_rel_volume_dir] = None
- fileutils.ensure_tree(mount_path)
- if not self._sofs_is_mounted():
- self._execute('mount', '-t', 'sofs', config, mount_path,
- run_as_root=True)
- if not self._sofs_is_mounted():
- msg = _("Cannot mount Scality SOFS, check syslog for errors")
- LOG.warning(msg)
- raise exception.VolumeBackendAPIException(data=msg)
+ def _get_mount_point_for_share(self, share=None):
+ # The _qemu_img_info_base() method from the RemoteFSSnapDriver class
+ # expects files (volume) to be inside a subdir of the mount point.
+ # So we have to append a dummy subdir.
+ return self.sofs_abs_volume_dir + "/00"
def _sofs_is_mounted(self):
- mount_path = self.configuration.scality_sofs_mount_point.rstrip('/')
+ """Check if SOFS is already mounted at the expected location."""
+ mount_path = self.sofs_mount_point.rstrip('/')
for mount in volume_utils.read_proc_mounts():
parts = mount.split()
if (parts[0].endswith('fuse') and
return True
return False
- def _size_bytes(self, size_in_g):
- return int(size_in_g) * units.Gi
-
- def _create_file(self, path, size):
- with open(path, "ab") as f:
- f.truncate(size)
- os.chmod(path, 0o666)
-
- def _copy_file(self, src_path, dest_path):
- self._execute('dd', 'if=%s' % src_path, 'of=%s' % dest_path,
- 'bs=1M', 'conv=fsync,nocreat,notrunc',
- run_as_root=True)
-
- def do_setup(self, context):
- """Any initialization the volume driver does while starting."""
- self._check_prerequisites()
- self._mount_sofs()
- voldir = os.path.join(self.configuration.scality_sofs_mount_point,
- self.configuration.scality_sofs_volume_dir)
- fileutils.ensure_tree(voldir)
+ @lockutils.synchronized('mount-sofs', 'cinder-sofs', external=True)
+ def _ensure_share_mounted(self, share=None):
+ """Mount SOFS if need be."""
+ fileutils.ensure_tree(self.sofs_mount_point)
- def check_for_setup_error(self):
- """Returns an error if prerequisites aren't met."""
- self._check_prerequisites()
- voldir = os.path.join(self.configuration.scality_sofs_mount_point,
- self.configuration.scality_sofs_volume_dir)
- if not os.path.isdir(voldir):
- msg = _("Cannot find volume dir for Scality SOFS at '%s'") % voldir
- LOG.warning(msg)
+ if not self._sofs_is_mounted():
+ self._execute('mount', '-t', 'sofs', self.sofs_config,
+ self.sofs_mount_point, run_as_root=True)
+ if not self._sofs_is_mounted():
+ msg = _("Cannot mount Scality SOFS, check syslog for errors")
+ LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
- def create_volume(self, volume):
- """Creates a logical volume.
-
- Can optionally return a Dictionary of changes to the volume
- object to be persisted.
- """
- self._create_file(self.local_path(volume),
- self._size_bytes(volume['size']))
- volume['provider_location'] = self._sofs_path(volume)
- return {'provider_location': volume['provider_location']}
-
- def create_volume_from_snapshot(self, volume, snapshot):
- """Creates a volume from a snapshot."""
- changes = self.create_volume(volume)
- self._copy_file(self.local_path(snapshot),
- self.local_path(volume))
- return changes
-
- def delete_volume(self, volume):
- """Deletes a logical volume."""
- os.remove(self.local_path(volume))
-
- def create_snapshot(self, snapshot):
- """Creates a snapshot."""
- volume_path = os.path.join(self.configuration.scality_sofs_mount_point,
- self.configuration.scality_sofs_volume_dir,
- snapshot['volume_name'])
- snapshot_path = self.local_path(snapshot)
- self._create_file(snapshot_path,
- self._size_bytes(snapshot['volume_size']))
- self._copy_file(volume_path, snapshot_path)
-
- def delete_snapshot(self, snapshot):
- """Deletes a snapshot."""
- os.remove(self.local_path(snapshot))
-
- def _sofs_path(self, volume):
- return os.path.join(self.configuration.scality_sofs_volume_dir,
- volume['name'])
-
- def local_path(self, volume):
- return os.path.join(self.configuration.scality_sofs_mount_point,
- self._sofs_path(volume))
-
- def ensure_export(self, context, volume):
- """Synchronously recreates an export for a logical volume."""
- pass
-
- def create_export(self, context, volume, connector):
- """Exports the volume.
-
- Can optionally return a Dictionary of changes to the volume
- object to be persisted.
- """
- pass
-
- def remove_export(self, context, volume):
- """Removes an export for a logical volume."""
- pass
-
- def initialize_connection(self, volume, connector):
- """Allow connection to connector and return connection info."""
- return {
- 'driver_volume_type': 'scality',
- 'data': {
- 'sofs_path': self._sofs_path(volume),
- 'export': self.configuration.scality_sofs_volume_dir,
- 'name': volume['name'],
- }
- }
-
- def terminate_connection(self, volume, connector, **kwargs):
- """Disallow connection from connector."""
- pass
+ fileutils.ensure_tree(self.sofs_abs_volume_dir)
- def detach_volume(self, context, volume, attachment=None):
- """Callback for volume detached."""
- pass
+ # We symlink the '00' subdir to its parent dir to maintain
+ # compatibility with previous version of this driver.
+ try:
+ os.symlink(".", self._get_mount_point_for_share())
+ except OSError as exc:
+ if exc.errno == errno.EEXIST:
+ if not os.path.islink(self._get_mount_point_for_share()):
+ raise
+ else:
+ raise
+
+ def _ensure_shares_mounted(self):
+ self._ensure_share_mounted()
+ self._mounted_shares = [self.sofs_rel_volume_dir]
+
+ def _find_share(self, volume_size_for):
+ try:
+ return self._mounted_shares[0]
+ except IndexError:
+ raise exception.RemoteFSNoSharesMounted()
def get_volume_stats(self, refresh=False):
- """Return the current state of the volume service.
-
- If 'refresh' is True, run the update first.
- """
+ """Return the current state of the volume service."""
stats = {
'vendor_name': 'Scality',
'driver_version': self.VERSION,
'reserved_percentage': 0,
}
backend_name = self.configuration.safe_get('volume_backend_name')
- stats['volume_backend_name'] = backend_name or 'Scality_SOFS'
+ stats['volume_backend_name'] = backend_name or self.volume_backend_name
return stats
- def copy_image_to_volume(self, context, volume, image_service, image_id):
- """Fetch the image from image_service and write it to the volume."""
- image_utils.fetch_to_raw(context,
- image_service,
- image_id,
- self.local_path(volume),
- self.configuration.volume_dd_blocksize,
- size=volume['size'])
- self.create_volume(volume)
-
- def copy_volume_to_image(self, context, volume, image_service, image_meta):
- """Copy the volume to the specified image."""
- image_utils.upload_volume(context,
- image_service,
- image_meta,
- self.local_path(volume))
-
- def clone_image(self, context, volume,
- image_location, image_meta,
- image_service):
- """Create a volume efficiently from an existing image.
-
- image_location is a string whose format depends on the
- image service backend in use. The driver should use it
- to determine whether cloning is possible.
-
- image_meta is the metadata associated with the image and
- includes properties like the image id, size, virtual-size
- etc.
-
- image_service is the reference of the image_service to use.
- Note that this is needed to be passed here for drivers that
- will want to fetch images from the image service directly.
-
- Returns a dict of volume properties eg. provider_location,
- boolean indicating whether cloning occurred
+ @remotefs_drv.locked_volume_id_operation
+ def initialize_connection(self, volume, connector):
+ """Allow connection to connector and return connection info."""
+
+ # Find active qcow2 file
+ active_file = self.get_active_image_from_info(volume)
+ path = '%s/%s' % (self._get_mount_point_for_share(), active_file)
+ sofs_rel_path = os.path.join(self.sofs_rel_volume_dir, "00",
+ volume['name'])
+
+ data = {'export': volume['provider_location'],
+ 'name': active_file,
+ 'sofs_path': sofs_rel_path}
+
+ # Test file for raw vs. qcow2 format
+ info = self._qemu_img_info(path, volume['name'])
+ data['format'] = info.file_format
+ if data['format'] not in ['raw', 'qcow2']:
+ msg = _('%s must be a valid raw or qcow2 image.') % path
+ raise exception.InvalidVolume(msg)
+
+ return {
+ 'driver_volume_type': self.driver_volume_type,
+ 'data': data,
+ 'mount_point_base': self.sofs_mount_point
+ }
+
+ def _qemu_img_info(self, path, volume_name):
+ return super(ScalityDriver, self)._qemu_img_info_base(
+ path, volume_name, self.sofs_abs_volume_dir)
+
+ @remotefs_drv.locked_volume_id_operation
+ def extend_volume(self, volume, size_gb):
+ volume_path = self.local_path(volume)
+
+ info = self._qemu_img_info(volume_path, volume['name'])
+ backing_fmt = info.file_format
+
+ if backing_fmt not in ['raw', 'qcow2']:
+ msg = _('Unrecognized backing format: %s')
+ raise exception.InvalidVolume(msg % backing_fmt)
+
+ # qemu-img can resize both raw and qcow2 files
+ image_utils.resize_image(volume_path, size_gb)
+
+ def _copy_volume_from_snapshot(self, snapshot, volume, volume_size):
+ """Copy data from snapshot to destination volume.
+
+ This is done with a qemu-img convert to raw/qcow2 from the snapshot
+ qcow2.
"""
- return None, False
- def create_cloned_volume(self, volume, src_vref):
- """Creates a clone of the specified volume."""
- self.create_volume_from_snapshot(volume, src_vref)
+ info_path = self._local_path_volume_info(snapshot['volume'])
+
+ # For BC compat' with version < 2 of this driver
+ try:
+ snap_info = self._read_info_file(info_path)
+ except IOError as exc:
+ if exc.errno != errno.ENOENT:
+ raise
+ else:
+ path_to_snap_img = self.local_path(snapshot)
+ else:
+ vol_path = self._local_volume_dir(snapshot['volume'])
+
+ forward_file = snap_info[snapshot['id']]
+ forward_path = os.path.join(vol_path, forward_file)
+
+ # Find the file which backs this file, which represents the point
+ # when this snapshot was created.
+ img_info = self._qemu_img_info(forward_path,
+ snapshot['volume']['name'])
+
+ path_to_snap_img = os.path.join(vol_path, img_info.backing_file)
+
+ LOG.debug("will copy from snapshot at %s", path_to_snap_img)
+
+ path_to_new_vol = self.local_path(volume)
+ out_format = 'raw'
+ image_utils.convert_image(path_to_snap_img,
+ path_to_new_vol,
+ out_format,
+ run_as_root=self._execute_as_root)
+
+ self._set_rw_permissions_for_all(path_to_new_vol)
- def extend_volume(self, volume, new_size):
- """Extend an existing volume."""
- self._create_file(self.local_path(volume),
- self._size_bytes(new_size))
+ image_utils.resize_image(path_to_new_vol, volume_size)
def backup_volume(self, context, backup, backup_service):
"""Create a new backup from an existing volume."""