cinder/cinder/tests/unit/test_scality.py

368 lines
16 KiB
Python

# 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
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Unit tests for the Scality SOFS Volume Driver.
"""
import errno
import os
import mock
from six.moves import urllib
from cinder import context
from cinder import exception
from cinder.openstack.common import imageutils
from cinder import test
from cinder.volume import configuration as conf
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."""
def setUp(self):
super(ScalityDriverTestCase, self).setUp()
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 = ''
self.drv = driver.ScalityDriver(configuration=self.cfg)
self.assertRaises(exception.VolumeBackendAPIException,
self.drv.check_for_setup_error)
exec_patcher = mock.patch.object(self.drv, '_execute',
mock.MagicMock())
exec_patcher.start()
self.addCleanup(exec_patcher.stop)
@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.drv.check_for_setup_error)
@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.drv.check_for_setup_error)
@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.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])
@mock.patch("cinder.volume.utils.read_proc_mounts")
@mock.patch("oslo_concurrency.processutils.execute")
@mock.patch("oslo_utils.fileutils.ensure_tree", mock.Mock())
@mock.patch("os.symlink", mock.Mock())
def test_ensure_shares_mounted_when_sofs_mounted(self, mock_execute,
mock_read_proc_mounts):
mock_read_proc_mounts.return_value = _FAKE_MOUNTS_TABLE[1]
self.drv._ensure_shares_mounted()
# Because SOFS is mounted from the beginning, we shouldn't read
# /proc/mounts more than once.
mock_read_proc_mounts.assert_called_once_with()
self.assertFalse(mock_execute.called)
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):
info = imageutils.QemuImgInfo()
info.file_format = 'qcow2'
mock_qemu_img_info.return_value = info
self.drv.db.volume_get.return_value = _FAKE_VOLUME
self.assertRaises(exception.InvalidVolume, self.drv.backup_volume,
context, _FAKE_BACKUP, mock.MagicMock())
mock_qemu_img_info.assert_called_once_with(_FAKE_VOL_PATH)
@mock.patch("cinder.image.image_utils.qemu_img_info")
def test_backup_volume_with_backing_file(self, mock_qemu_img_info):
info = imageutils.QemuImgInfo()
info.file_format = 'raw'
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__())