From 136507615cc4b407f5da2ffa324843657ba3dc01 Mon Sep 17 00:00:00 2001 From: Mike Perez Date: Mon, 3 Aug 2015 08:17:18 -0700 Subject: [PATCH] Revert "Revert First version of Cinder driver for Quobyte" This reverts commit e896ae29eccc8575aca08e4ebeb27a82e28fa8eb. Change-Id: Id6b25af58434ecef891c2deecc1a574d351ee713 --- cinder/tests/unit/test_quobyte.py | 933 ++++++++++++++++++++++++++++++ cinder/volume/drivers/quobyte.py | 439 ++++++++++++++ 2 files changed, 1372 insertions(+) create mode 100644 cinder/tests/unit/test_quobyte.py create mode 100644 cinder/volume/drivers/quobyte.py diff --git a/cinder/tests/unit/test_quobyte.py b/cinder/tests/unit/test_quobyte.py new file mode 100644 index 000000000..7ff3910b3 --- /dev/null +++ b/cinder/tests/unit/test_quobyte.py @@ -0,0 +1,933 @@ +# 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 errno +import os +import six +import traceback + +import mock +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._configuration = mock.Mock(conf.Configuration) + self._configuration.append_config_values(mock.ANY) + 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._driver =\ + quobyte.QuobyteDriver(configuration=self._configuration, + db=FakeDb()) + self._driver.shares = {} + self._driver.set_nas_security_options(is_new_cinder_install=False) + + def assertRaisesAndMessageMatches( + self, excClass, msg, callableObj, *args, **kwargs): + """Ensure that the specified exception was raised. """ + + 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.format_exc())) + self.assertIn(msg, six.text_type(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 mock.patch.object(self._driver, '_execute') as mock_execute, \ + mock.patch('cinder.volume.drivers.quobyte.QuobyteDriver' + '.read_proc_mount') as mock_open: + # Content of /proc/mount (not mounted yet). + mock_open.return_value = six.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) + + getfattr_call = mock.call( + 'getfattr', '-n', 'quobyte.info', self.TEST_MNT_POINT, + run_as_root=False) + + mock_execute.assert_has_calls( + [mkdir_call, mount_call, getfattr_call], any_order=False) + + def test_mount_quobyte_already_mounted_detected_seen_in_proc_mount(self): + with mock.patch.object(self._driver, '_execute') as mock_execute, \ + mock.patch('cinder.volume.drivers.quobyte.QuobyteDriver' + '.read_proc_mount') as mock_open: + # Content of /proc/mount (already mounted). + mock_open.return_value = six.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)) + + self._driver._mount_quobyte(self.TEST_QUOBYTE_VOLUME, + self.TEST_MNT_POINT) + + mock_execute.assert_called_once_with( + 'getfattr', '-n', 'quobyte.info', self.TEST_MNT_POINT, + run_as_root=False) + + def test_mount_quobyte_should_suppress_and_log_already_mounted_error(self): + """test_mount_quobyte_should_suppress_and_log_already_mounted_error + + 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 mock.patch.object(self._driver, '_execute') as mock_execute, \ + mock.patch('cinder.volume.drivers.quobyte.QuobyteDriver' + '.read_proc_mount') as mock_open, \ + mock.patch('cinder.volume.drivers.quobyte.LOG') as mock_LOG: + # Content of /proc/mount (empty). + mock_open.return_value = six.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.warning.assert_called_once_with('%s is already mounted', + self.TEST_QUOBYTE_VOLUME) + + def test_mount_quobyte_should_reraise_already_mounted_error(self): + """test_mount_quobyte_should_reraise_already_mounted_error + + Like test_mount_quobyte_should_suppress_and_log_already_mounted_error + but with ensure=False. + """ + with mock.patch.object(self._driver, '_execute') as mock_execute, \ + mock.patch('cinder.volume.drivers.quobyte.QuobyteDriver' + '.read_proc_mount') as mock_open: + mock_open.return_value = six.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.""" + 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 + + drv._get_mount_point_for_share = mock.Mock(return_value=self. + TEST_MNT_POINT) + + drv._execute = mock.Mock(return_value=(df_output, None)) + + self.assertEqual((df_avail, df_total_size), + drv._get_available_capacity(self.TEST_QUOBYTE_VOLUME)) + (drv._get_mount_point_for_share. + assert_called_once_with(self.TEST_QUOBYTE_VOLUME)) + (drv._execute. + assert_called_once_with('df', + '--portability', + '--block-size', + '1', + self.TEST_MNT_POINT, + run_as_root=self._driver._execute_as_root)) + + 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 takes the Volume URL 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 URL is without quobyte://.""" + 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 mock.patch.object(self._driver, '_get_mount_point_for_share') as \ + mock_get_mount_point, \ + mock.patch.object(self._driver, '_mount_quobyte') as \ + 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(mock.create_autospec(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.""" + drv = self._driver + drv._execute = mock.Mock(side_effect=OSError + (errno.ENOENT, 'No such file or directory')) + + self.assertRaisesAndMessageMatches(exception.VolumeDriverException, + 'mount.quobyte is not installed', + drv.check_for_setup_error) + drv._execute.assert_called_once_with('mount.quobyte', + check_exit_code=False, + run_as_root=False) + + def test_check_for_setup_error_throws_client_not_executable(self): + """check_for_setup_error throws if client cannot be executed.""" + drv = self._driver + + drv._execute = mock.Mock(side_effect=OSError + (errno.EPERM, 'Operation not permitted')) + + self.assertRaisesAndMessageMatches(OSError, + 'Operation not permitted', + drv.check_for_setup_error) + drv._execute.assert_called_once_with('mount.quobyte', + check_exit_code=False, + run_as_root=False) + + 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 no space is 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): + drv = self._driver + volume = self._simple_volume() + + drv._create_sparsed_file = mock.Mock() + drv._set_rw_permissions_for_all = mock.Mock() + + drv._do_create_volume(volume) + drv._create_sparsed_file.assert_called_once_with(mock.ANY, mock.ANY) + drv._set_rw_permissions_for_all.assert_called_once_with(mock.ANY) + + def test_create_nonsparsed_volume(self): + drv = self._driver + volume = self._simple_volume() + + old_value = self._configuration.quobyte_sparsed_volumes + self._configuration.quobyte_sparsed_volumes = False + + drv._create_regular_file = mock.Mock() + drv._set_rw_permissions_for_all = mock.Mock() + + drv._do_create_volume(volume) + drv._create_regular_file.assert_called_once_with(mock.ANY, mock.ANY) + drv._set_rw_permissions_for_all.assert_called_once_with(mock.ANY) + + self._configuration.quobyte_sparsed_volumes = old_value + + def test_create_qcow2_volume(self): + drv = self._driver + + volume = self._simple_volume() + old_value = self._configuration.quobyte_qcow2_volumes + self._configuration.quobyte_qcow2_volumes = True + + drv._execute = mock.Mock() + + hashed = drv._get_hash_str(volume['provider_location']) + path = '%s/%s/volume-%s' % (self.TEST_MNT_POINT_BASE, + hashed, + self.VOLUME_UUID) + + drv._do_create_volume(volume) + + assert_calls = [mock.call('qemu-img', 'create', '-f', 'qcow2', + '-o', 'preallocation=metadata', path, + str(volume['size'] * units.Gi), + run_as_root=self._driver._execute_as_root), + mock.call('chmod', 'ugo+rw', path, + run_as_root=self._driver._execute_as_root)] + drv._execute.assert_has_calls(assert_calls) + + 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.""" + drv = self._driver + + drv.LOG = mock.Mock() + drv._find_share = mock.Mock() + drv._do_create_volume = mock.Mock() + drv._ensure_shares_mounted = mock.Mock() + + volume = DumbVolume() + volume['size'] = self.TEST_SIZE_IN_GB + drv.create_volume(volume) + + drv._find_share.assert_called_once_with(mock.ANY) + drv._do_create_volume.assert_called_once_with(volume) + drv._ensure_shares_mounted.assert_called_once_with() + + def test_create_volume_should_return_provider_location(self): + """create_volume should return provider_location with found share.""" + drv = self._driver + + drv.LOG = mock.Mock() + drv._ensure_shares_mounted = mock.Mock() + drv._do_create_volume = mock.Mock() + drv._find_share = mock.Mock(return_value=self.TEST_QUOBYTE_VOLUME) + + volume = DumbVolume() + volume['size'] = self.TEST_SIZE_IN_GB + result = drv.create_volume(volume) + self.assertEqual(self.TEST_QUOBYTE_VOLUME, result['provider_location']) + + drv._do_create_volume.assert_called_once_with(volume) + drv._ensure_shares_mounted.assert_called_once_with() + drv._find_share.assert_called_once_with(self.TEST_SIZE_IN_GB) + + def test_create_cloned_volume(self): + drv = self._driver + + drv._create_snapshot = mock.Mock() + drv._copy_volume_from_snapshot = mock.Mock() + drv._delete_snapshot = mock.Mock() + + 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_cloned_volume(volume, src_vref) + + drv._create_snapshot.assert_called_once_with(snap_ref) + drv._copy_volume_from_snapshot.assert_called_once_with(snap_ref, + volume_ref, + volume['size']) + drv._delete_snapshot.assert_called_once_with(mock.ANY) + + @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 mock.patch.object(self._driver, '_ensure_share_mounted') as \ + mock_ensure_share_mounted, \ + mock.patch.object(self._driver, '_local_volume_dir') as \ + mock_local_volume_dir, \ + mock.patch.object(self._driver, + 'get_active_image_from_info') as \ + mock_active_image_from_info, \ + mock.patch.object(self._driver, '_execute') as \ + mock_execute, \ + mock.patch.object(self._driver, '_local_path_volume') as \ + mock_local_path_volume, \ + mock.patch.object(self._driver, '_local_path_volume_info') as \ + 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._driver._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.""" + drv = self._driver + + drv._execute = mock.Mock() + + volume = DumbVolume() + volume['name'] = 'volume-123' + volume['provider_location'] = self.TEST_QUOBYTE_VOLUME + + drv._ensure_share_mounted = mock.Mock() + + drv.delete_volume(volume) + + (drv._ensure_share_mounted. + assert_called_once_with(self.TEST_QUOBYTE_VOLUME)) + drv._execute.assert_called_once_with('rm', '-f', + mock.ANY, + run_as_root=False) + + def test_delete_should_not_delete_if_provider_location_not_provided(self): + """delete_volume shouldn't delete if provider_location missed.""" + drv = self._driver + + drv._ensure_share_mounted = mock.Mock() + drv._execute = mock.Mock() + + volume = DumbVolume() + volume['name'] = 'volume-123' + volume['provider_location'] = None + + drv.delete_volume(volume) + + assert not drv._ensure_share_mounted.called + assert not drv._execute.called + + def test_extend_volume(self): + drv = 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) + + drv.get_active_image_from_info = mock.Mock(return_value=volume['name']) + image_utils.qemu_img_info = mock.Mock(return_value=img_info) + image_utils.resize_image = mock.Mock() + + drv.extend_volume(volume, 3) + + drv.get_active_image_from_info.assert_called_once_with(volume) + image_utils.qemu_img_info.assert_called_once_with(volume_path) + image_utils.resize_image.assert_called_once_with(volume_path, 3) + + def test_copy_volume_from_snapshot(self): + drv = self._driver + + # lots of test vars to be prepared at first + 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'] + + 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) + + # mocking and testing starts here + image_utils.convert_image = mock.Mock() + drv._read_info_file = mock.Mock(return_value= + {'active': snap_file, + snapshot['id']: snap_file}) + image_utils.qemu_img_info = mock.Mock(return_value=img_info) + drv._set_rw_permissions_for_all = mock.Mock() + + drv._copy_volume_from_snapshot(snapshot, dest_volume, size) + + drv._read_info_file.assert_called_once_with(info_path) + image_utils.qemu_img_info.assert_called_once_with(snap_path) + (image_utils.convert_image. + assert_called_once_with(src_vol_path, + dest_vol_path, + 'raw', + run_as_root=self._driver._execute_as_root)) + drv._set_rw_permissions_for_all.assert_called_once_with(dest_vol_path) + + 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): + 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': 'available'} + + new_volume = DumbVolume() + new_volume['size'] = snap_ref['size'] + + drv._ensure_shares_mounted = mock.Mock() + drv._find_share = mock.Mock(return_value=self.TEST_QUOBYTE_VOLUME) + drv._do_create_volume = mock.Mock() + drv._copy_volume_from_snapshot = mock.Mock() + + drv.create_volume_from_snapshot(new_volume, snap_ref) + + drv._ensure_shares_mounted.assert_called_once_with() + drv._find_share.assert_called_once_with(new_volume['size']) + drv._do_create_volume.assert_called_once_with(new_volume) + (drv._copy_volume_from_snapshot. + assert_called_once_with(snap_ref, new_volume, new_volume['size'])) + + def test_initialize_connection(self): + drv = 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) + + drv.get_active_image_from_info = mock.Mock(return_value=volume['name']) + image_utils.qemu_img_info = mock.Mock(return_value=img_info) + + conn_info = drv.initialize_connection(volume, None) + + drv.get_active_image_from_info.assert_called_once_with(volume) + image_utils.qemu_img_info.assert_called_once_with(vol_path) + + 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 mock.patch.object(drv, 'get_active_image_from_info') as \ + mock_get_active_image_from_info, \ + mock.patch.object(drv, '_local_volume_dir') as \ + mock_local_volume_dir, \ + mock.patch.object(image_utils, 'qemu_img_info') as \ + mock_qemu_img_info, \ + mock.patch.object(image_utils, 'upload_volume') as \ + mock_upload_volume, \ + mock.patch.object(image_utils, 'create_temporary_file') as \ + 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) + self.assertTrue(mock_create_temporary_file.called) + + 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 mock.patch.object(drv, 'get_active_image_from_info') as \ + mock_get_active_image_from_info, \ + mock.patch.object(drv, '_local_volume_dir') as \ + mock_local_volume_dir, \ + mock.patch.object(image_utils, 'qemu_img_info') as \ + mock_qemu_img_info, \ + mock.patch.object(image_utils, 'convert_image') as \ + mock_convert_image, \ + mock.patch.object(image_utils, 'upload_volume') as \ + mock_upload_volume, \ + mock.patch.object(image_utils, 'create_temporary_file') as \ + 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) + self.assertTrue(mock_create_temporary_file.called) + + 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 mock.patch.object(drv, 'get_active_image_from_info') as \ + mock_get_active_image_from_info, \ + mock.patch.object(drv, '_local_volume_dir') as \ + mock_local_volume_dir, \ + mock.patch.object(image_utils, 'qemu_img_info') as \ + mock_qemu_img_info, \ + mock.patch.object(image_utils, 'convert_image') as \ + mock_convert_image, \ + mock.patch.object(image_utils, 'upload_volume') as \ + mock_upload_volume, \ + mock.patch.object(image_utils, 'create_temporary_file') as \ + 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) + self.assertTrue(mock_create_temporary_file.called) diff --git a/cinder/volume/drivers/quobyte.py b/cinder/volume/drivers/quobyte.py new file mode 100644 index 000000000..2b2760476 --- /dev/null +++ b/cinder/volume/drivers/quobyte.py @@ -0,0 +1,439 @@ +# 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 +from oslo_log import log as logging + +from cinder import compute +from cinder import exception +from cinder.i18n import _, _LI, _LW +from cinder.image import image_utils +from cinder.openstack.common import fileutils +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 = (_("There's no Quobyte volume configured (%s). Example:" + " quobyte:///") % + 'quobyte_volume_url') + LOG.warning(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 + + 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, ", + {'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.warning(_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", 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) + + @utils.synchronized('quobyte_ensure', external=False) + 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', 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.warning(_LW("Failed to unmount previous mount: " + "%s"), exc) + else: + # TODO(quobyte): Extend exc analysis in here? + LOG.warning(_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.warning(_LW("%s is already mounted"), quobyte_volume) + else: + raise + + if mounted: + self._validate_volume(mount_path) + + def _validate_volume(self, mount_path): + """Wraps execute calls for checking validity of a Quobyte volume""" + command = ['getfattr', "-n", "quobyte.info", mount_path] + try: + self._execute(*command, run_as_root=False) + except processutils.ProcessExecutionError as exc: + msg = (_("The mount %(mount_path)s is not a valid" + " Quobyte USP volume. Error: %(exc)s") + % {'mount_path': mount_path, 'exc': exc}) + raise exception.VolumeDriverException(msg) + + if not os.access(mount_path, os.W_OK | os.X_OK): + LOG.warning(_LW("Volume is not writable. Please broaden the file" + " permissions. Mount: %s"), mount_path) -- 2.45.2