
Use versioned objects in remotefs.py, which is base classes for all drivers with an interface of remote filesystem. Update glusterfs, nfs, smbfs, quobyte, scality, vzstorage volume drivers to use versionedobjects instead of dicts. Please note that netapp_nfs driver wasn't fixed, just unit tests were adapted to pass. Change-Id: Iebd20544102672d7b7ca820c9e9005833ec2580e Depends-On: I661ef85755e9c75aaedb4f157165d2514de3e9ec
393 lines
17 KiB
Python
393 lines
17 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 oslo_utils import imageutils
|
|
from six.moves import urllib
|
|
|
|
from cinder import context
|
|
from cinder import exception
|
|
from cinder import test
|
|
from cinder.tests.unit import fake_snapshot
|
|
from cinder.tests.unit import fake_volume
|
|
from cinder.volume import configuration as conf
|
|
import cinder.volume.drivers.scality as driver
|
|
|
|
_FAKE_VOLUME_ID = 'a79d463e-1fd5-11e5-a6ff-5b81bfee8544'
|
|
_FAKE_VOLUME_NAME = 'volume-a79d463e-1fd5-11e5-a6ff-5b81bfee8544'
|
|
|
|
_FAKE_SNAPSHOT_ID = 'ae3d6da2-1fd5-11e5-967f-1b8cf3b401ab'
|
|
_FAKE_SNAPSHOT_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()
|
|
|
|
self.context = context.get_admin_context()
|
|
|
|
def _simple_volume(self, **kwargs):
|
|
updates = {'display_name': _FAKE_VOLUME_NAME,
|
|
'id': _FAKE_VOLUME_ID,
|
|
'provider_location': 'fake_share'}
|
|
updates.update(kwargs)
|
|
|
|
return fake_volume.fake_volume_obj(self.context, **updates)
|
|
|
|
def _simple_snapshot(self, **kwargs):
|
|
updates = {'id': _FAKE_SNAPSHOT_ID,
|
|
'display_name': _FAKE_SNAPSHOT_NAME,
|
|
'status': 'available',
|
|
'provider_location': None,
|
|
'volume_size': 1}
|
|
|
|
updates.update(kwargs)
|
|
snapshot = fake_snapshot.fake_snapshot_obj(self.context, **updates)
|
|
volume = self._simple_volume()
|
|
snapshot.volume = volume
|
|
|
|
return snapshot
|
|
|
|
@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
|
|
|
|
volume = self._simple_volume()
|
|
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(volume, None)
|
|
|
|
expected_conn_info = {
|
|
'driver_volume_type': driver.ScalityDriver.driver_volume_type,
|
|
'mount_point_base': _FAKE_MNT_POINT,
|
|
'data': {
|
|
'export': volume.provider_location,
|
|
'name': volume.name,
|
|
'sofs_path': 'cinder/volumes/00/' + volume.name,
|
|
'format': 'raw'
|
|
}
|
|
}
|
|
self.assertEqual(expected_conn_info, conn_info)
|
|
mock_get_active_image_from_info.assert_called_once_with(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(self._simple_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, self._simple_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(self._simple_snapshot(),
|
|
self._simple_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 = self._simple_volume(
|
|
display_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(self._simple_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 = self._simple_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 = self._simple_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 = self._simple_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, self._simple_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__())
|