diff --git a/cinder/exception.py b/cinder/exception.py index 5edad3ff835..70f9c3d045a 100755 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -814,6 +814,15 @@ class NetAppDriverException(VolumeDriverException): message = _("NetApp Cinder Driver exception.") +# Quobyte USP +class QuobyteException(VolumeDriverException): + message = _("Unknown Quobyte exception") + + +class QuobyteVolumeNotMounted(NotFound): + message = _("No mounted Quobyte volumes found") + + class EMCVnxCLICmdError(VolumeBackendAPIException): def __init__(self, cmd=None, rc=None, out='', log_as_error=True, **kwargs): diff --git a/cinder/tests/test_quobyte.py b/cinder/tests/test_quobyte.py new file mode 100644 index 00000000000..7520802b773 --- /dev/null +++ b/cinder/tests/test_quobyte.py @@ -0,0 +1,1014 @@ +# Copyright (c) 2014 Quobyte Inc. +# Copyright (c) 2013 Red Hat, Inc. +# 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 Quobyte driver module.""" + +import contextlib +import errno +import os +import StringIO +import traceback + +import mock +import mox as mox_lib +from mox import IgnoreArg +from mox import IsA +from mox import stubout +from oslo.concurrency import processutils as putils +from oslo.config import cfg +from oslo.utils import units + +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 quobyte + + +CONF = cfg.CONF + + +class DumbVolume(object): + fields = {} + + def __setitem__(self, key, value): + self.fields[key] = value + + def __getitem__(self, item): + return self.fields[item] + + +class FakeDb(object): + msg = "Tests are broken: mock this out." + + def volume_get(self, *a, **kw): + raise Exception(self.msg) + + def snapshot_get_all_for_volume(self, *a, **kw): + """Mock this if you want results from it.""" + return [] + + +class QuobyteDriverTestCase(test.TestCase): + """Test case for Quobyte driver.""" + + TEST_QUOBYTE_VOLUME = 'quobyte://quobyte-host/openstack-volumes' + TEST_QUOBYTE_VOLUME_WITHOUT_PROTOCOL = 'quobyte-host/openstack-volumes' + TEST_SIZE_IN_GB = 1 + TEST_MNT_POINT = '/mnt/quobyte' + TEST_MNT_POINT_BASE = '/mnt' + TEST_LOCAL_PATH = '/mnt/quobyte/volume-123' + TEST_FILE_NAME = 'test.txt' + TEST_SHARES_CONFIG_FILE = '/etc/cinder/test-shares.conf' + TEST_TMP_FILE = '/tmp/tempfile' + VOLUME_UUID = 'abcdefab-cdef-abcd-efab-cdefabcdefab' + SNAP_UUID = 'bacadaca-baca-daca-baca-dacadacadaca' + SNAP_UUID_2 = 'bebedede-bebe-dede-bebe-dedebebedede' + + def setUp(self): + super(QuobyteDriverTestCase, self).setUp() + self._mox = mox_lib.Mox() + self._configuration = mox_lib.MockObject(conf.Configuration) + self._configuration.append_config_values(mox_lib.IgnoreArg()) + self._configuration.quobyte_volume_url = \ + self.TEST_QUOBYTE_VOLUME + self._configuration.quobyte_client_cfg = None + self._configuration.quobyte_sparsed_volumes = True + self._configuration.quobyte_qcow2_volumes = False + self._configuration.quobyte_mount_point_base = \ + self.TEST_MNT_POINT_BASE + + self.stubs = stubout.StubOutForTesting() + self._driver =\ + quobyte.QuobyteDriver(configuration=self._configuration, + db=FakeDb()) + self._driver.shares = {} + self._driver.set_nas_security_options(is_new_cinder_install=False) + self.execute_as_root = False + self.addCleanup(self._mox.UnsetStubs) + + def stub_out_not_replaying(self, obj, attr_name): + attr_to_replace = getattr(obj, attr_name) + stub = mox_lib.MockObject(attr_to_replace) + self.stubs.Set(obj, attr_name, stub) + + def assertRaisesAndMessageMatches( + self, excClass, msg, callableObj, *args, **kwargs): + """Ensure that the specified exception was raised and its message + includes the string 'msg'. + """ + + caught = False + try: + callableObj(*args, **kwargs) + except Exception as exc: + caught = True + self.assertEqual(excClass, type(exc), + 'Wrong exception caught: %s Stacktrace: %s' % + (exc, traceback.print_exc())) + self.assertIn(msg, str(exc)) + + if not caught: + self.fail('Expected raised exception but nothing caught.') + + def test_local_path(self): + """local_path common use case.""" + drv = self._driver + + volume = DumbVolume() + volume['provider_location'] = self.TEST_QUOBYTE_VOLUME + volume['name'] = 'volume-123' + + self.assertEqual( + '/mnt/1331538734b757ed52d0e18c0a7210cd/volume-123', + drv.local_path(volume)) + + def test_mount_quobyte_should_mount_correctly(self): + with contextlib.nested( + mock.patch.object(self._driver, '_execute'), + mock.patch('cinder.volume.drivers.quobyte.QuobyteDriver' + '.read_proc_mount'), + mock.patch('xattr.getxattr') + ) as (mock_execute, mock_open, mock_getxattr): + # Content of /proc/mount (not mounted yet). + mock_open.return_value = StringIO.StringIO( + "/dev/sda5 / ext4 rw,relatime,data=ordered 0 0") + + self._driver._mount_quobyte(self.TEST_QUOBYTE_VOLUME, + self.TEST_MNT_POINT) + + mkdir_call = mock.call('mkdir', '-p', self.TEST_MNT_POINT) + + mount_call = mock.call( + 'mount.quobyte', self.TEST_QUOBYTE_VOLUME, + self.TEST_MNT_POINT, run_as_root=False) + mock_execute.assert_has_calls([mkdir_call, mount_call], + any_order=False) + mock_getxattr.assert_called_once_with(self.TEST_MNT_POINT, + 'quobyte.info') + + def test_mount_quobyte_already_mounted_detected_seen_in_proc_mount(self): + with contextlib.nested( + mock.patch.object(self._driver, '_execute'), + mock.patch('cinder.volume.drivers.quobyte.QuobyteDriver' + '.read_proc_mount'), + mock.patch('xattr.getxattr') + ) as (mock_execute, mock_open, mock_getxattr): + # Content of /proc/mount (already mounted). + mock_open.return_value = StringIO.StringIO( + "quobyte@%s %s fuse rw,nosuid,nodev,noatime,user_id=1000" + ",group_id=100,default_permissions,allow_other 0 0" + % (self.TEST_QUOBYTE_VOLUME, self.TEST_MNT_POINT)) + mock_getxattr.return_value = "non-empty string" + + self._driver._mount_quobyte(self.TEST_QUOBYTE_VOLUME, + self.TEST_MNT_POINT) + + self.assertFalse(mock_execute.called) + mock_getxattr.assert_called_once_with(self.TEST_MNT_POINT, + 'quobyte.info') + + def test_mount_quobyte_should_suppress_and_log_already_mounted_error(self): + """Based on /proc/mount, the file system is not mounted yet. However, + mount.quobyte returns with an 'already mounted' error. + This is a last-resort safe-guard in case /proc/mount parsing was not + successful. + + Because _mount_quobyte gets called with ensure=True, the error will + be suppressed and logged instead. + """ + with contextlib.nested( + mock.patch.object(self._driver, '_execute'), + mock.patch('cinder.volume.drivers.quobyte.QuobyteDriver' + '.read_proc_mount'), + mock.patch('cinder.volume.drivers.quobyte.LOG') + ) as (mock_execute, mock_open, mock_LOG): + # Content of /proc/mount (empty). + mock_open.return_value = StringIO.StringIO() + mock_execute.side_effect = [None, putils.ProcessExecutionError( + stderr='is busy or already mounted')] + + self._driver._mount_quobyte(self.TEST_QUOBYTE_VOLUME, + self.TEST_MNT_POINT, + ensure=True) + + mkdir_call = mock.call('mkdir', '-p', self.TEST_MNT_POINT) + mount_call = mock.call( + 'mount.quobyte', self.TEST_QUOBYTE_VOLUME, + self.TEST_MNT_POINT, run_as_root=False) + mock_execute.assert_has_calls([mkdir_call, mount_call], + any_order=False) + + mock_LOG.warn.assert_called_once_with('%s is already mounted', + self.TEST_QUOBYTE_VOLUME) + + def test_mount_quobyte_should_reraise_already_mounted_error(self): + """Same as + test_mount_quobyte_should_suppress_and_log_already_mounted_error + but with ensure=False. + """ + with contextlib.nested( + mock.patch.object(self._driver, '_execute'), + mock.patch('cinder.volume.drivers.quobyte.QuobyteDriver' + '.read_proc_mount') + ) as (mock_execute, mock_open): + mock_open.return_value = StringIO.StringIO() + mock_execute.side_effect = [ + None, # mkdir + putils.ProcessExecutionError( # mount + stderr='is busy or already mounted')] + + self.assertRaises(putils.ProcessExecutionError, + self._driver._mount_quobyte, + self.TEST_QUOBYTE_VOLUME, + self.TEST_MNT_POINT, + ensure=False) + + mkdir_call = mock.call('mkdir', '-p', self.TEST_MNT_POINT) + mount_call = mock.call( + 'mount.quobyte', self.TEST_QUOBYTE_VOLUME, + self.TEST_MNT_POINT, run_as_root=False) + mock_execute.assert_has_calls([mkdir_call, mount_call], + any_order=False) + + def test_get_hash_str(self): + """_get_hash_str should calculation correct value.""" + drv = self._driver + + self.assertEqual('1331538734b757ed52d0e18c0a7210cd', + drv._get_hash_str(self.TEST_QUOBYTE_VOLUME)) + + def test_get_available_capacity_with_df(self): + """_get_available_capacity should calculate correct value.""" + mox = self._mox + drv = self._driver + + df_total_size = 2620544 + df_avail = 1490560 + df_head = 'Filesystem 1K-blocks Used Available Use% Mounted on\n' + df_data = 'quobyte@%s %d 996864 %d 41%% %s' % \ + (self.TEST_QUOBYTE_VOLUME, df_total_size, df_avail, + self.TEST_MNT_POINT) + df_output = df_head + df_data + + mox.StubOutWithMock(drv, '_get_mount_point_for_share') + drv._get_mount_point_for_share(self.TEST_QUOBYTE_VOLUME).\ + AndReturn(self.TEST_MNT_POINT) + + mox.StubOutWithMock(drv, '_execute') + drv._execute('df', '--portability', '--block-size', '1', + self.TEST_MNT_POINT, + run_as_root=self.execute_as_root).AndReturn((df_output, + None)) + + mox.ReplayAll() + + self.assertEqual((df_avail, df_total_size), + drv._get_available_capacity(self.TEST_QUOBYTE_VOLUME)) + + mox.VerifyAll() + + def test_get_capacity_info(self): + with mock.patch.object(self._driver, '_get_available_capacity') \ + as mock_get_available_capacity: + drv = self._driver + + df_size = 2620544 + df_avail = 1490560 + + mock_get_available_capacity.return_value = (df_avail, df_size) + + size, available, used = drv._get_capacity_info(mock.ANY) + + mock_get_available_capacity.assert_called_once_with(mock.ANY) + + self.assertEqual(df_size, size) + self.assertEqual(df_avail, available) + self.assertEqual(size - available, used) + + def test_load_shares_config(self): + """_load_shares_config only puts the Volume URL into shares and strips + quobyte://. + """ + drv = self._driver + + drv._load_shares_config() + + self.assertIn(self.TEST_QUOBYTE_VOLUME_WITHOUT_PROTOCOL, drv.shares) + + def test_load_shares_config_without_protocol(self): + """Same as test_load_shares_config, but this time the URL was specified + without quobyte:// in front. + """ + drv = self._driver + + drv.configuration.quobyte_volume_url = \ + self.TEST_QUOBYTE_VOLUME_WITHOUT_PROTOCOL + + drv._load_shares_config() + + self.assertIn(self.TEST_QUOBYTE_VOLUME_WITHOUT_PROTOCOL, drv.shares) + + def test_ensure_share_mounted(self): + """_ensure_share_mounted simple use case.""" + with contextlib.nested( + mock.patch.object(self._driver, '_get_mount_point_for_share'), + mock.patch.object(self._driver, '_mount_quobyte') + ) as (mock_get_mount_point, mock_mount): + drv = self._driver + drv._ensure_share_mounted(self.TEST_QUOBYTE_VOLUME) + + mock_get_mount_point.assert_called_once_with( + self.TEST_QUOBYTE_VOLUME) + mock_mount.assert_called_once_with( + self.TEST_QUOBYTE_VOLUME, + mock_get_mount_point.return_value, + ensure=True) + + def test_ensure_shares_mounted_should_save_mounting_successfully(self): + """_ensure_shares_mounted should save share if mounted with success.""" + with mock.patch.object(self._driver, '_ensure_share_mounted') \ + as mock_ensure_share_mounted: + drv = self._driver + + drv._ensure_shares_mounted() + + mock_ensure_share_mounted.assert_called_once_with( + self.TEST_QUOBYTE_VOLUME_WITHOUT_PROTOCOL) + self.assertIn(self.TEST_QUOBYTE_VOLUME_WITHOUT_PROTOCOL, + drv._mounted_shares) + + def test_ensure_shares_mounted_should_not_save_mounting_with_error(self): + """_ensure_shares_mounted should not save if mount raised an error.""" + with mock.patch.object(self._driver, '_ensure_share_mounted') \ + as mock_ensure_share_mounted: + drv = self._driver + + mock_ensure_share_mounted.side_effect = Exception() + + drv._ensure_shares_mounted() + + mock_ensure_share_mounted.assert_called_once_with( + self.TEST_QUOBYTE_VOLUME_WITHOUT_PROTOCOL) + self.assertEqual(1, len(drv.shares)) + self.assertEqual(0, len(drv._mounted_shares)) + + def test_do_setup(self): + """do_setup runs successfully.""" + drv = self._driver + drv.do_setup(IsA(context.RequestContext)) + + def test_check_for_setup_error_throws_quobyte_volume_url_not_set(self): + """check_for_setup_error throws if 'quobyte_volume_url' is not set.""" + drv = self._driver + + drv.configuration.quobyte_volume_url = None + + self.assertRaisesAndMessageMatches(exception.VolumeDriverException, + 'no Quobyte volume configured', + drv.check_for_setup_error) + + def test_check_for_setup_error_throws_client_not_installed(self): + """check_for_setup_error throws if client is not installed.""" + mox = self._mox + drv = self._driver + + mox.StubOutWithMock(drv, '_execute') + drv._execute('mount.quobyte', check_exit_code=False, + run_as_root=False).\ + AndRaise(OSError(errno.ENOENT, 'No such file or directory')) + + mox.ReplayAll() + + self.assertRaisesAndMessageMatches(exception.VolumeDriverException, + 'mount.quobyte is not installed', + drv.check_for_setup_error) + + mox.VerifyAll() + + def test_check_for_setup_error_throws_client_not_executable(self): + """check_for_setup_error throws if client cannot be executed.""" + mox = self._mox + drv = self._driver + + mox.StubOutWithMock(drv, '_execute') + drv._execute('mount.quobyte', check_exit_code=False, + run_as_root=False).\ + AndRaise(OSError(errno.EPERM, 'Operation not permitted')) + + mox.ReplayAll() + + self.assertRaisesAndMessageMatches(OSError, + 'Operation not permitted', + drv.check_for_setup_error) + + mox.VerifyAll() + + def test_find_share_should_throw_error_if_there_is_no_mounted_shares(self): + """_find_share should throw error if there is no mounted share.""" + drv = self._driver + + drv._mounted_shares = [] + + self.assertRaises(exception.NotFound, + drv._find_share, + self.TEST_SIZE_IN_GB) + + def test_find_share(self): + """_find_share simple use case.""" + drv = self._driver + + drv._mounted_shares = [self.TEST_QUOBYTE_VOLUME] + + self.assertEqual(self.TEST_QUOBYTE_VOLUME, + drv._find_share(self.TEST_SIZE_IN_GB)) + + def test_find_share_does_not_throw_error_if_there_isnt_enough_space(self): + """_find_share intentionally does not throw when df reports no + available space left. + """ + with mock.patch.object(self._driver, '_get_available_capacity') \ + as mock_get_available_capacity: + drv = self._driver + + df_size = 2620544 + df_avail = 0 + mock_get_available_capacity.return_value = (df_avail, df_size) + + drv._mounted_shares = [self.TEST_QUOBYTE_VOLUME] + + self.assertEqual(self.TEST_QUOBYTE_VOLUME, + drv._find_share(self.TEST_SIZE_IN_GB)) + + # The current implementation does not call _get_available_capacity. + # Future ones might do and therefore we mocked it. + self.assertGreaterEqual(mock_get_available_capacity.call_count, 0) + + def _simple_volume(self, uuid=None): + volume = DumbVolume() + volume['provider_location'] = self.TEST_QUOBYTE_VOLUME + if uuid is None: + volume['id'] = self.VOLUME_UUID + else: + volume['id'] = uuid + # volume['name'] mirrors format from db/sqlalchemy/models.py + volume['name'] = 'volume-%s' % volume['id'] + volume['size'] = 10 + volume['status'] = 'available' + + return volume + + def test_create_sparsed_volume(self): + mox = self._mox + drv = self._driver + volume = self._simple_volume() + + mox.StubOutWithMock(drv, '_create_sparsed_file') + mox.StubOutWithMock(drv, '_set_rw_permissions_for_all') + + drv._create_sparsed_file(IgnoreArg(), IgnoreArg()) + drv._set_rw_permissions_for_all(IgnoreArg()) + + mox.ReplayAll() + + drv._do_create_volume(volume) + + mox.VerifyAll() + + def test_create_nonsparsed_volume(self): + mox = self._mox + drv = self._driver + volume = self._simple_volume() + + old_value = self._configuration.quobyte_sparsed_volumes + self._configuration.quobyte_sparsed_volumes = False + + mox.StubOutWithMock(drv, '_create_regular_file') + mox.StubOutWithMock(drv, '_set_rw_permissions_for_all') + + drv._create_regular_file(IgnoreArg(), IgnoreArg()) + drv._set_rw_permissions_for_all(IgnoreArg()) + + mox.ReplayAll() + + drv._do_create_volume(volume) + + mox.VerifyAll() + + self._configuration.quobyte_sparsed_volumes = old_value + + def test_create_qcow2_volume(self): + (mox, drv) = self._mox, self._driver + + volume = self._simple_volume() + old_value = self._configuration.quobyte_qcow2_volumes + self._configuration.quobyte_qcow2_volumes = True + + mox.StubOutWithMock(drv, '_execute') + + hashed = drv._get_hash_str(volume['provider_location']) + path = '%s/%s/volume-%s' % (self.TEST_MNT_POINT_BASE, + hashed, + self.VOLUME_UUID) + + drv._execute('qemu-img', 'create', '-f', 'qcow2', + '-o', 'preallocation=metadata', path, + str(volume['size'] * units.Gi), + run_as_root=self.execute_as_root) + + drv._execute('chmod', 'ugo+rw', path, run_as_root=self.execute_as_root) + + mox.ReplayAll() + + drv._do_create_volume(volume) + + mox.VerifyAll() + + self._configuration.quobyte_qcow2_volumes = old_value + + def test_create_volume_should_ensure_quobyte_mounted(self): + """create_volume ensures shares provided in config are mounted.""" + mox = self._mox + drv = self._driver + + self.stub_out_not_replaying(quobyte, 'LOG') + self.stub_out_not_replaying(drv, '_find_share') + self.stub_out_not_replaying(drv, '_do_create_volume') + + mox.StubOutWithMock(drv, '_ensure_shares_mounted') + drv._ensure_shares_mounted() + + mox.ReplayAll() + + volume = DumbVolume() + volume['size'] = self.TEST_SIZE_IN_GB + drv.create_volume(volume) + + mox.VerifyAll() + + def test_create_volume_should_return_provider_location(self): + """create_volume should return provider_location with found share.""" + mox = self._mox + drv = self._driver + + self.stub_out_not_replaying(quobyte, 'LOG') + self.stub_out_not_replaying(drv, '_ensure_shares_mounted') + self.stub_out_not_replaying(drv, '_do_create_volume') + + mox.StubOutWithMock(drv, '_find_share') + drv._find_share(self.TEST_SIZE_IN_GB).\ + AndReturn(self.TEST_QUOBYTE_VOLUME) + + mox.ReplayAll() + + volume = DumbVolume() + volume['size'] = self.TEST_SIZE_IN_GB + result = drv.create_volume(volume) + self.assertEqual(self.TEST_QUOBYTE_VOLUME, result['provider_location']) + + mox.VerifyAll() + + def test_create_cloned_volume(self): + (mox, drv) = self._mox, self._driver + + mox.StubOutWithMock(drv, '_create_snapshot') + mox.StubOutWithMock(drv, '_delete_snapshot') + mox.StubOutWithMock(drv, '_read_info_file') + mox.StubOutWithMock(image_utils, 'convert_image') + mox.StubOutWithMock(drv, '_copy_volume_from_snapshot') + + volume = self._simple_volume() + src_vref = self._simple_volume() + src_vref['id'] = '375e32b2-804a-49f2-b282-85d1d5a5b9e1' + src_vref['name'] = 'volume-%s' % src_vref['id'] + volume_ref = {'id': volume['id'], + 'name': volume['name'], + 'status': volume['status'], + 'provider_location': volume['provider_location'], + 'size': volume['size']} + + snap_ref = {'volume_name': src_vref['name'], + 'name': 'clone-snap-%s' % src_vref['id'], + 'size': src_vref['size'], + 'volume_size': src_vref['size'], + 'volume_id': src_vref['id'], + 'id': 'tmp-snap-%s' % src_vref['id'], + 'volume': src_vref} + + drv._create_snapshot(snap_ref) + + drv._copy_volume_from_snapshot(snap_ref, volume_ref, volume['size']) + + drv._delete_snapshot(mox_lib.IgnoreArg()) + + mox.ReplayAll() + + drv.create_cloned_volume(volume, src_vref) + + mox.VerifyAll() + + @mock.patch('cinder.openstack.common.fileutils.delete_if_exists') + def test_delete_volume(self, mock_delete_if_exists): + volume = self._simple_volume() + volume_filename = 'volume-%s' % self.VOLUME_UUID + volume_path = '%s/%s' % (self.TEST_MNT_POINT, volume_filename) + info_file = volume_path + '.info' + + with contextlib.nested( + mock.patch.object(self._driver, '_ensure_share_mounted'), + mock.patch.object(self._driver, '_local_volume_dir'), + mock.patch.object(self._driver, 'get_active_image_from_info'), + mock.patch.object(self._driver, '_execute'), + mock.patch.object(self._driver, '_local_path_volume'), + mock.patch.object(self._driver, '_local_path_volume_info') + ) as (mock_ensure_share_mounted, mock_local_volume_dir, + mock_active_image_from_info, mock_execute, + mock_local_path_volume, mock_local_path_volume_info): + mock_local_volume_dir.return_value = self.TEST_MNT_POINT + mock_active_image_from_info.return_value = volume_filename + mock_local_path_volume.return_value = volume_path + mock_local_path_volume_info.return_value = info_file + + self._driver.delete_volume(volume) + + mock_ensure_share_mounted.assert_called_once_with( + volume['provider_location']) + mock_local_volume_dir.assert_called_once_with(volume) + mock_active_image_from_info.assert_called_once_with(volume) + mock_execute.assert_called_once_with( + 'rm', '-f', volume_path, run_as_root=self.execute_as_root) + mock_local_path_volume_info.assert_called_once_with(volume) + mock_local_path_volume.assert_called_once_with(volume) + mock_delete_if_exists.assert_any_call(volume_path) + mock_delete_if_exists.assert_any_call(info_file) + + def test_delete_should_ensure_share_mounted(self): + """delete_volume should ensure that corresponding share is mounted.""" + mox = self._mox + drv = self._driver + + self.stub_out_not_replaying(drv, '_execute') + + volume = DumbVolume() + volume['name'] = 'volume-123' + volume['provider_location'] = self.TEST_QUOBYTE_VOLUME + + mox.StubOutWithMock(drv, '_ensure_share_mounted') + drv._ensure_share_mounted(self.TEST_QUOBYTE_VOLUME) + + mox.ReplayAll() + + drv.delete_volume(volume) + + mox.VerifyAll() + + def test_delete_should_not_delete_if_provider_location_not_provided(self): + """delete_volume shouldn't delete if provider_location missed.""" + mox = self._mox + drv = self._driver + + self.stub_out_not_replaying(drv, '_ensure_share_mounted') + + volume = DumbVolume() + volume['name'] = 'volume-123' + volume['provider_location'] = None + + mox.StubOutWithMock(drv, '_execute') + + mox.ReplayAll() + + drv.delete_volume(volume) + + mox.VerifyAll() + + def test_extend_volume(self): + (mox, drv) = self._mox, self._driver + + volume = self._simple_volume() + + volume_path = '%s/%s/volume-%s' % (self.TEST_MNT_POINT_BASE, + drv._get_hash_str( + self.TEST_QUOBYTE_VOLUME), + self.VOLUME_UUID) + + qemu_img_info_output = """image: volume-%s + file format: qcow2 + virtual size: 1.0G (1073741824 bytes) + disk size: 473K + """ % self.VOLUME_UUID + + img_info = imageutils.QemuImgInfo(qemu_img_info_output) + + mox.StubOutWithMock(drv, '_execute') + mox.StubOutWithMock(drv, 'get_active_image_from_info') + mox.StubOutWithMock(image_utils, 'qemu_img_info') + mox.StubOutWithMock(image_utils, 'resize_image') + + drv.get_active_image_from_info(volume).AndReturn(volume['name']) + + image_utils.qemu_img_info(volume_path).AndReturn(img_info) + + image_utils.resize_image(volume_path, 3) + + mox.ReplayAll() + + drv.extend_volume(volume, 3) + + mox.VerifyAll() + + def test_copy_volume_from_snapshot(self): + (mox, drv) = self._mox, self._driver + + mox.StubOutWithMock(image_utils, 'convert_image') + mox.StubOutWithMock(drv, '_read_info_file') + mox.StubOutWithMock(image_utils, 'qemu_img_info') + mox.StubOutWithMock(drv, '_set_rw_permissions_for_all') + + dest_volume = self._simple_volume( + 'c1073000-0000-0000-0000-0000000c1073') + src_volume = self._simple_volume() + + vol_dir = os.path.join(self.TEST_MNT_POINT_BASE, + drv._get_hash_str(self.TEST_QUOBYTE_VOLUME)) + src_vol_path = os.path.join(vol_dir, src_volume['name']) + dest_vol_path = os.path.join(vol_dir, dest_volume['name']) + info_path = os.path.join(vol_dir, src_volume['name']) + '.info' + + snapshot = {'volume_name': src_volume['name'], + 'name': 'clone-snap-%s' % src_volume['id'], + 'size': src_volume['size'], + 'volume_size': src_volume['size'], + 'volume_id': src_volume['id'], + 'id': 'tmp-snap-%s' % src_volume['id'], + 'volume': src_volume} + + snap_file = dest_volume['name'] + '.' + snapshot['id'] + snap_path = os.path.join(vol_dir, snap_file) + + size = dest_volume['size'] + + drv._read_info_file(info_path).AndReturn( + {'active': snap_file, + snapshot['id']: snap_file} + ) + + qemu_img_output = """image: %s + file format: raw + virtual size: 1.0G (1073741824 bytes) + disk size: 173K + backing file: %s + """ % (snap_file, src_volume['name']) + img_info = imageutils.QemuImgInfo(qemu_img_output) + + image_utils.qemu_img_info(snap_path).AndReturn(img_info) + + image_utils.convert_image(src_vol_path, + dest_vol_path, + 'raw', + run_as_root=self.execute_as_root) + + drv._set_rw_permissions_for_all(dest_vol_path) + + mox.ReplayAll() + + drv._copy_volume_from_snapshot(snapshot, dest_volume, size) + + mox.VerifyAll() + + def test_create_volume_from_snapshot_status_not_available(self): + """Expect an error when the snapshot's status is not 'available'.""" + drv = self._driver + + src_volume = self._simple_volume() + snap_ref = {'volume_name': src_volume['name'], + 'name': 'clone-snap-%s' % src_volume['id'], + 'size': src_volume['size'], + 'volume_size': src_volume['size'], + 'volume_id': src_volume['id'], + 'id': 'tmp-snap-%s' % src_volume['id'], + 'volume': src_volume, + 'status': 'error'} + + new_volume = DumbVolume() + new_volume['size'] = snap_ref['size'] + + self.assertRaises(exception.InvalidSnapshot, + drv.create_volume_from_snapshot, + new_volume, + snap_ref) + + def test_create_volume_from_snapshot(self): + (mox, drv) = self._mox, self._driver + + src_volume = self._simple_volume() + snap_ref = {'volume_name': src_volume['name'], + 'name': 'clone-snap-%s' % src_volume['id'], + 'size': src_volume['size'], + 'volume_size': src_volume['size'], + 'volume_id': src_volume['id'], + 'id': 'tmp-snap-%s' % src_volume['id'], + 'volume': src_volume, + 'status': 'available'} + + new_volume = DumbVolume() + new_volume['size'] = snap_ref['size'] + + mox.StubOutWithMock(drv, '_ensure_shares_mounted') + mox.StubOutWithMock(drv, '_find_share') + mox.StubOutWithMock(drv, '_do_create_volume') + mox.StubOutWithMock(drv, '_copy_volume_from_snapshot') + + drv._ensure_shares_mounted() + + drv._find_share(new_volume['size']).AndReturn(self.TEST_QUOBYTE_VOLUME) + + drv._do_create_volume(new_volume) + drv._copy_volume_from_snapshot(snap_ref, + new_volume, + new_volume['size']) + + mox.ReplayAll() + + drv.create_volume_from_snapshot(new_volume, snap_ref) + + mox.VerifyAll() + + def test_initialize_connection(self): + (mox, drv) = self._mox, self._driver + + volume = self._simple_volume() + vol_dir = os.path.join(self.TEST_MNT_POINT_BASE, + drv._get_hash_str(self.TEST_QUOBYTE_VOLUME)) + vol_path = os.path.join(vol_dir, volume['name']) + + qemu_img_output = """image: %s + file format: raw + virtual size: 1.0G (1073741824 bytes) + disk size: 173K + """ % volume['name'] + img_info = imageutils.QemuImgInfo(qemu_img_output) + + mox.StubOutWithMock(drv, 'get_active_image_from_info') + mox.StubOutWithMock(image_utils, 'qemu_img_info') + + drv.get_active_image_from_info(volume).AndReturn(volume['name']) + image_utils.qemu_img_info(vol_path).AndReturn(img_info) + + mox.ReplayAll() + + conn_info = drv.initialize_connection(volume, None) + + mox.VerifyAll() + + self.assertEqual(conn_info['data']['format'], 'raw') + self.assertEqual(conn_info['driver_volume_type'], 'quobyte') + self.assertEqual(conn_info['data']['name'], volume['name']) + self.assertEqual(conn_info['mount_point_base'], + self.TEST_MNT_POINT_BASE) + + def test_copy_volume_to_image_raw_image(self): + drv = self._driver + + volume = self._simple_volume() + volume_path = '%s/%s' % (self.TEST_MNT_POINT, volume['name']) + image_meta = {'id': '10958016-e196-42e3-9e7f-5d8927ae3099'} + + with contextlib.nested( + mock.patch.object(drv, 'get_active_image_from_info'), + mock.patch.object(drv, '_local_volume_dir'), + mock.patch.object(image_utils, 'qemu_img_info'), + mock.patch.object(image_utils, 'upload_volume'), + mock.patch.object(image_utils, 'create_temporary_file') + ) as (mock_get_active_image_from_info, mock_local_volume_dir, + mock_qemu_img_info, mock_upload_volume, + mock_create_temporary_file): + mock_get_active_image_from_info.return_value = volume['name'] + + mock_local_volume_dir.return_value = self.TEST_MNT_POINT + + mock_create_temporary_file.return_value = self.TEST_TMP_FILE + + qemu_img_output = """image: %s + file format: raw + virtual size: 1.0G (1073741824 bytes) + disk size: 173K + """ % volume['name'] + img_info = imageutils.QemuImgInfo(qemu_img_output) + mock_qemu_img_info.return_value = img_info + + upload_path = volume_path + + drv.copy_volume_to_image(mock.ANY, volume, mock.ANY, image_meta) + + mock_get_active_image_from_info.assert_called_once_with(volume) + mock_local_volume_dir.assert_called_once_with(volume) + mock_qemu_img_info.assert_called_once_with(volume_path) + mock_upload_volume.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY, upload_path) + mock_create_temporary_file.assert_once_called_with() + + def test_copy_volume_to_image_qcow2_image(self): + """Upload a qcow2 image file which has to be converted to raw first.""" + drv = self._driver + + volume = self._simple_volume() + volume_path = '%s/%s' % (self.TEST_MNT_POINT, volume['name']) + image_meta = {'id': '10958016-e196-42e3-9e7f-5d8927ae3099'} + + with contextlib.nested( + mock.patch.object(drv, 'get_active_image_from_info'), + mock.patch.object(drv, '_local_volume_dir'), + mock.patch.object(image_utils, 'qemu_img_info'), + mock.patch.object(image_utils, 'convert_image'), + mock.patch.object(image_utils, 'upload_volume'), + mock.patch.object(image_utils, 'create_temporary_file') + ) as (mock_get_active_image_from_info, mock_local_volume_dir, + mock_qemu_img_info, mock_convert_image, mock_upload_volume, + mock_create_temporary_file): + mock_get_active_image_from_info.return_value = volume['name'] + + mock_local_volume_dir.return_value = self.TEST_MNT_POINT + + mock_create_temporary_file.return_value = self.TEST_TMP_FILE + + qemu_img_output = """image: %s + file format: qcow2 + virtual size: 1.0G (1073741824 bytes) + disk size: 173K + """ % volume['name'] + img_info = imageutils.QemuImgInfo(qemu_img_output) + mock_qemu_img_info.return_value = img_info + + upload_path = self.TEST_TMP_FILE + + drv.copy_volume_to_image(mock.ANY, volume, mock.ANY, image_meta) + + mock_get_active_image_from_info.assert_called_once_with(volume) + mock_local_volume_dir.assert_called_with(volume) + mock_qemu_img_info.assert_called_once_with(volume_path) + mock_convert_image.assert_called_once_with( + volume_path, upload_path, 'raw') + mock_upload_volume.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY, upload_path) + mock_create_temporary_file.assert_once_called_with() + + def test_copy_volume_to_image_snapshot_exists(self): + """Upload an active snapshot which has to be converted to raw first.""" + drv = self._driver + + volume = self._simple_volume() + volume_path = '%s/volume-%s' % (self.TEST_MNT_POINT, self.VOLUME_UUID) + volume_filename = 'volume-%s' % self.VOLUME_UUID + image_meta = {'id': '10958016-e196-42e3-9e7f-5d8927ae3099'} + + with contextlib.nested( + mock.patch.object(drv, 'get_active_image_from_info'), + mock.patch.object(drv, '_local_volume_dir'), + mock.patch.object(image_utils, 'qemu_img_info'), + mock.patch.object(image_utils, 'convert_image'), + mock.patch.object(image_utils, 'upload_volume'), + mock.patch.object(image_utils, 'create_temporary_file') + ) as (mock_get_active_image_from_info, mock_local_volume_dir, + mock_qemu_img_info, mock_convert_image, mock_upload_volume, + mock_create_temporary_file): + mock_get_active_image_from_info.return_value = volume['name'] + + mock_local_volume_dir.return_value = self.TEST_MNT_POINT + + mock_create_temporary_file.return_value = self.TEST_TMP_FILE + + qemu_img_output = """image: volume-%s.%s + file format: qcow2 + virtual size: 1.0G (1073741824 bytes) + disk size: 173K + backing file: %s + """ % (self.VOLUME_UUID, self.SNAP_UUID, volume_filename) + img_info = imageutils.QemuImgInfo(qemu_img_output) + mock_qemu_img_info.return_value = img_info + + upload_path = self.TEST_TMP_FILE + + drv.copy_volume_to_image(mock.ANY, volume, mock.ANY, image_meta) + + mock_get_active_image_from_info.assert_called_once_with(volume) + mock_local_volume_dir.assert_called_with(volume) + mock_qemu_img_info.assert_called_once_with(volume_path) + mock_convert_image.assert_called_once_with( + volume_path, upload_path, 'raw') + mock_upload_volume.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY, upload_path) + mock_create_temporary_file.assert_once_called_with() diff --git a/cinder/volume/drivers/quobyte.py b/cinder/volume/drivers/quobyte.py new file mode 100644 index 00000000000..71a6d9cd956 --- /dev/null +++ b/cinder/volume/drivers/quobyte.py @@ -0,0 +1,430 @@ +# Copyright (c) 2014 Quobyte Inc. +# Copyright (c) 2013 Red Hat, Inc. +# 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. + +import errno +import os + +from oslo.concurrency import processutils +from oslo.config import cfg +import xattr + +from cinder import compute +from cinder import exception +from cinder.i18n import _, _LE, _LI, _LW +from cinder.image import image_utils +from cinder.openstack.common import fileutils +from cinder.openstack.common import log as logging +from cinder import utils +from cinder.volume.drivers import remotefs as remotefs_drv + +VERSION = '1.0' + +LOG = logging.getLogger(__name__) + +volume_opts = [ + cfg.StrOpt('quobyte_volume_url', + default=None, + help=('URL to the Quobyte volume e.g.,' + ' quobyte://