--- /dev/null
+# 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)
--- /dev/null
+# 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://<DIR host>/<volume name>')),
+ 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://<DIR host>/<volume name>") %
+ '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)