From 49d92764183e288b8f62b91a51179c307dd19a44 Mon Sep 17 00:00:00 2001 From: Silvan Kaiser Date: Wed, 26 Nov 2014 17:29:01 +0100 Subject: [PATCH] First version of Cinder driver for Quobyte USP Supported Operations are: - Create Volume - Delete Volume - Attach Volume - Detach Volume - Extend Volume - Create Snapshot - Delete Snapshot - List Snapshots - Create Volume from Snapshot - Create Volume from Image - Create Volume from Volume (Clone) - Create Image from Volume The driver uses a file-based interface to access the configured Quobyte volume. Therefore, the driver is similar to the existing drivers NFS and GlusterFS. Due to the similarities, I reused the snapshot code from the GlusterFS driver. Gluster, thanks for that! I've kept the "Red Hat" copyright in the header to credit you properly. All driver functions are covered by unit tests. Snapshot tests were taken over from test_glusterfs.py. New tests are written using "Mock" instead of "mox". Certification tests: https://bugs.launchpad.net/cinder/+bug/1401471 Implements: blueprint quobyte-usp-driver Change-Id: I7ca13e28b000d7a07c2baecd5454e50be4c9640b --- cinder/exception.py | 9 + cinder/tests/test_quobyte.py | 1014 ++++++++++++++++++++++++++++++ cinder/volume/drivers/quobyte.py | 430 +++++++++++++ requirements.txt | 1 + 4 files changed, 1454 insertions(+) create mode 100644 cinder/tests/test_quobyte.py create mode 100644 cinder/volume/drivers/quobyte.py diff --git a/cinder/exception.py b/cinder/exception.py index 5edad3ff8..70f9c3d04 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 000000000..7520802b7 --- /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 000000000..71a6d9cd9 --- /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:///')), + cfg.StrOpt('quobyte_client_cfg', + default=None, + help=('Path to a Quobyte Client configuration file.')), + cfg.BoolOpt('quobyte_sparsed_volumes', + default=True, + help=('Create volumes as sparse files which take no space.' + ' If set to False, volume is created as regular file.' + 'In such case volume creation takes a lot of time.')), + cfg.BoolOpt('quobyte_qcow2_volumes', + default=True, + help=('Create volumes as QCOW2 files rather than raw files.')), + cfg.StrOpt('quobyte_mount_point_base', + default='$state_path/mnt', + help=('Base dir containing the mount point' + ' for the Quobyte volume.')), +] + +CONF = cfg.CONF +CONF.register_opts(volume_opts) + + +class QuobyteDriver(remotefs_drv.RemoteFSSnapDriver): + """Cinder driver for Quobyte USP. + + Volumes are stored as files on the mounted Quobyte volume. The hypervisor + will expose them as block devices. + + Unlike other similar drivers, this driver uses exactly one Quobyte volume + because Quobyte USP is a distributed storage system. To add or remove + capacity, administrators can add or remove storage servers to/from the + volume. + + For different types of volumes e.g., SSD vs. rotating disks, + use multiple backends in Cinder. + + Note: To be compliant with the inherited RemoteFSSnapDriver, Quobyte + volumes are also referred to as shares. + + Version history: + 1.0 - Initial driver. + """ + + driver_volume_type = 'quobyte' + driver_prefix = 'quobyte' + volume_backend_name = 'Quobyte' + VERSION = VERSION + + def __init__(self, execute=processutils.execute, *args, **kwargs): + super(QuobyteDriver, self).__init__(*args, **kwargs) + self.configuration.append_config_values(volume_opts) + + # Used to manage snapshots which are currently attached to a VM. + self._nova = None + + def do_setup(self, context): + """Any initialization the volume driver does while starting.""" + self.set_nas_security_options(is_new_cinder_install=False) + super(QuobyteDriver, self).do_setup(context) + + self.shares = {} # address : options + self._nova = compute.API() + + def check_for_setup_error(self): + if not self.configuration.quobyte_volume_url: + msg = (_LW("There's no Quobyte volume configured (%s). Example:" + " quobyte:///") % + 'quobyte_volume_url') + LOG.warn(msg) + raise exception.VolumeDriverException(msg) + + # Check if mount.quobyte is installed + try: + self._execute('mount.quobyte', check_exit_code=False, + run_as_root=False) + except OSError as exc: + if exc.errno == errno.ENOENT: + raise exception.VolumeDriverException( + 'mount.quobyte is not installed') + else: + raise exc + + def set_nas_security_options(self, is_new_cinder_install): + self.configuration.nas_secure_file_operations = 'true' + self.configuration.nas_secure_file_permissions = 'true' + self._execute_as_root = False + + def _qemu_img_info(self, path, volume_name): + return super(QuobyteDriver, self)._qemu_img_info_base( + path, volume_name, self.configuration.quobyte_mount_point_base) + + @utils.synchronized('quobyte', external=False) + def create_cloned_volume(self, volume, src_vref): + """Creates a clone of the specified volume.""" + self._create_cloned_volume(volume, src_vref) + + @utils.synchronized('quobyte', external=False) + def create_volume(self, volume): + return super(QuobyteDriver, self).create_volume(volume) + + @utils.synchronized('quobyte', external=False) + def create_volume_from_snapshot(self, volume, snapshot): + return self._create_volume_from_snapshot(volume, snapshot) + + 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. + """ + + LOG.debug("snapshot: %(snap)s, volume: %(vol)s, " + "volume_size: %(size)s" + % {'snap': snapshot['id'], + 'vol': volume['id'], + 'size': volume_size}) + + info_path = self._local_path_volume_info(snapshot['volume']) + snap_info = self._read_info_file(info_path) + 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) + + path_to_new_vol = self._local_path_volume(volume) + + LOG.debug("will copy from snapshot at %s" % path_to_snap_img) + + if self.configuration.quobyte_qcow2_volumes: + out_format = 'qcow2' + else: + 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) + + @utils.synchronized('quobyte', external=False) + def delete_volume(self, volume): + """Deletes a logical volume.""" + + if not volume['provider_location']: + LOG.warn(_LW('Volume %s does not have provider_location ' + 'specified, skipping'), volume['name']) + return + + self._ensure_share_mounted(volume['provider_location']) + + volume_dir = self._local_volume_dir(volume) + mounted_path = os.path.join(volume_dir, + self.get_active_image_from_info(volume)) + + self._execute('rm', '-f', mounted_path, + run_as_root=self._execute_as_root) + + # If an exception (e.g. timeout) occurred during delete_snapshot, the + # base volume may linger around, so just delete it if it exists + base_volume_path = self._local_path_volume(volume) + fileutils.delete_if_exists(base_volume_path) + + info_path = self._local_path_volume_info(volume) + fileutils.delete_if_exists(info_path) + + @utils.synchronized('quobyte', external=False) + def create_snapshot(self, snapshot): + """Apply locking to the create snapshot operation.""" + + return self._create_snapshot(snapshot) + + @utils.synchronized('quobyte', external=False) + def delete_snapshot(self, snapshot): + """Apply locking to the delete snapshot operation.""" + self._delete_snapshot(snapshot) + + @utils.synchronized('quobyte', external=False) + 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/%s' % (self.configuration.quobyte_mount_point_base, + self._get_hash_str(volume['provider_location']), + active_file) + + data = {'export': volume['provider_location'], + 'name': active_file} + if volume['provider_location'] in self.shares: + data['options'] = self.shares[volume['provider_location']] + + # 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': 'quobyte', + 'data': data, + 'mount_point_base': self.configuration.quobyte_mount_point_base + } + + @utils.synchronized('quobyte', external=False) + def copy_volume_to_image(self, context, volume, image_service, image_meta): + self._copy_volume_to_image(context, volume, image_service, + image_meta) + + @utils.synchronized('quobyte', external=False) + def extend_volume(self, volume, size_gb): + volume_path = self.local_path(volume) + volume_filename = os.path.basename(volume_path) + + # Ensure no snapshots exist for the volume + active_image = self.get_active_image_from_info(volume) + if volume_filename != active_image: + msg = _('Extend volume is only supported for this' + ' driver when no snapshots exist.') + raise exception.InvalidVolume(msg) + + 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 _do_create_volume(self, volume): + """Create a volume on given Quobyte volume. + + :param volume: volume reference + """ + volume_path = self.local_path(volume) + volume_size = volume['size'] + + if self.configuration.quobyte_qcow2_volumes: + self._create_qcow2_file(volume_path, volume_size) + else: + if self.configuration.quobyte_sparsed_volumes: + self._create_sparsed_file(volume_path, volume_size) + else: + self._create_regular_file(volume_path, volume_size) + + self._set_rw_permissions_for_all(volume_path) + + def _load_shares_config(self, share_file=None): + """Put 'quobyte_volume_url' into the 'shares' list. + :param share_file: string, Not used because the user has to specify the + the Quobyte volume directly. + """ + self.shares = {} + + url = self.configuration.quobyte_volume_url + + # Strip quobyte:// from the URL + protocol = self.driver_volume_type + "://" + if url.startswith(protocol): + url = url[len(protocol):] + + self.shares[url] = None # None = No extra mount options. + + LOG.debug("Quobyte Volume URL set to: %s" % str(self.shares)) + + def _ensure_share_mounted(self, quobyte_volume): + """Mount Quobyte volume. + :param quobyte_volume: string + """ + mount_path = self._get_mount_point_for_share(quobyte_volume) + self._mount_quobyte(quobyte_volume, mount_path, ensure=True) + + def _ensure_shares_mounted(self): + """Mount the Quobyte volume. + + Used for example by RemoteFsDriver._update_volume_stats + """ + self._mounted_shares = [] + + self._load_shares_config() + + for share in self.shares.keys(): + try: + self._ensure_share_mounted(share) + self._mounted_shares.append(share) + except Exception as exc: + LOG.warning(_LW('Exception during mounting %s') % (exc,)) + + LOG.debug('Available shares %s' % str(self._mounted_shares)) + + def _find_share(self, volume_size_in_gib): + """Returns the mounted Quobyte volume. + + Multiple shares are not supported because the virtualization of + multiple storage devices is taken care of at the level of Quobyte USP. + + For different types of volumes e.g., SSD vs. rotating disks, use + multiple backends in Cinder. + + :param volume_size_in_gib: int size in GB. Ignored by this driver. + """ + + if not self._mounted_shares: + raise exception.NotFound() + + assert len(self._mounted_shares) == 1, 'There must be exactly' \ + ' one Quobyte volume.' + target_volume = self._mounted_shares[0] + + LOG.debug('Selected %s as target Quobyte volume.' % target_volume) + + return target_volume + + def _get_mount_point_for_share(self, quobyte_volume): + """Return mount point for Quobyte volume. + :param quobyte_volume: Example: storage-host/openstack-volumes + """ + return os.path.join(self.configuration.quobyte_mount_point_base, + self._get_hash_str(quobyte_volume)) + + # open() wrapper to mock reading from /proc/mount. + @staticmethod + def read_proc_mount(): # pragma: no cover + return open('/proc/mounts') + + def _mount_quobyte(self, quobyte_volume, mount_path, ensure=False): + """Mount Quobyte volume to mount path.""" + mounted = False + for l in QuobyteDriver.read_proc_mount(): + if l.split()[1] == mount_path: + mounted = True + break + + if mounted: + try: + os.stat(mount_path) + except OSError as exc: + if exc.errno == errno.ENOTCONN: + mounted = False + try: + LOG.info(_LI('Fixing previous mount %s which was not' + ' unmounted correctly.') % mount_path) + self._execute('umount.quobyte', mount_path, + run_as_root=False) + except processutils.ProcessExecutionError as exc: + LOG.warn(_LW("Failed to unmount previous mount: %s"), + exc) + else: + # TODO(quobyte): Extend exc analysis in here? + LOG.warn(_LW("Unknown error occurred while checking mount" + " point: %s Trying to continue."), exc) + + if not mounted: + if not os.path.isdir(mount_path): + self._execute('mkdir', '-p', mount_path) + + command = ['mount.quobyte', quobyte_volume, mount_path] + if self.configuration.quobyte_client_cfg: + command.extend(['-c', self.configuration.quobyte_client_cfg]) + + try: + LOG.info(_LI('Mounting volume: %s ...') % quobyte_volume) + self._execute(*command, run_as_root=False) + LOG.info(_LI('Mounting volume: %s succeeded') % quobyte_volume) + mounted = True + except processutils.ProcessExecutionError as exc: + if ensure and 'already mounted' in exc.stderr: + LOG.warn(_LW("%s is already mounted"), quobyte_volume) + else: + raise + + if mounted: + try: + xattr.getxattr(mount_path, 'quobyte.info') + except Exception as exc: + msg = _LE("The mount %(mount_path)s is not a valid" + " Quobyte USP volume. Error: %(exc)s") \ + % {'mount_path': mount_path, 'exc': exc} + raise exception.QuobyteException(msg) + if not os.access(mount_path, os.W_OK | os.X_OK): + LOG.warn(_LW("Volume is not writable. Please broaden the file" + " permissions. Mount: %s"), mount_path) diff --git a/requirements.txt b/requirements.txt index b346e3a55..e8e54e33e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,3 +41,4 @@ suds>=0.4 WebOb>=1.2.3 wsgiref>=0.1.2 oslo.i18n>=1.0.0 # Apache-2.0 +xattr>=0.4 -- 2.45.2